方便大家學習的Node.js教程(一):理解Node.js

来源:http://www.cnblogs.com/jiaoyu121/archive/2017/06/10/6980218.html
-Advertisement-
Play Games

理解Node.js 為了理解Node.js是如何工作的,首先你需要理解一些使得Javascript適用於伺服器端開發的關鍵特性。Javascript是一門簡單而又靈活的語言,這種靈活性讓它能夠經受住時間的考驗。函數、閉包等特性使Javascript成為一門適合Web開發的理想語言。 有一種偏見認為J ...


 

 

理解Node.js

為了理解Node.js是如何工作的,首先你需要理解一些使得Javascript適用於伺服器端開發的關鍵特性。Javascript是一門簡單而又靈活的語言,這種靈活性讓它能夠經受住時間的考驗。函數、閉包等特性使Javascript成為一門適合Web開發的理想語言。

有一種偏見認為Javascript是不可靠的,然而事實並非如此。人們對Javascript的偏見來源於DOM,DOM是瀏覽器廠商提供的用於Javascript與瀏覽器交互的API,不同瀏覽器廠商實現的DOM存在差異。然而,Javascript本身是一門定義清晰的語言,可以在不同的瀏覽器及Node.js中運行。本節,我會先介紹一些Javascript的基礎以及Node.js是如何使用Javascript提供了一個性能優異的Web開發平臺。

變數

Javascript使用var關鍵字定義變數。例如下麵的代碼創建了一個名為foo的變數,併在命令行中輸出。(可以通過node variable.js在命令行中執行下麵的代碼文件。)

代碼文件 variable.js
var foo = 123;
console.log(foo); // 123

Javascript運行環境(瀏覽器或者Node.js)通常會定義一些我們可以使用的全局變數,例如console對象,console對象包含一個成員函數loglog函數能夠接受任意數量的參數並輸出它們。我們接下來會遇到更多的全局對象,你將會發現,Javascript具有一個優秀的編程語言應該包含的大部分特性。

數值

Javascript支持常見的算數操作符(+-*/%)。例如下列代碼:

var foo = 3;
var bar = 5;
console.log(foo+1); //4
console.log(foo / bar); //0.6
console.log(foo * bar); //15
console.log(foo - bar); //-2
console.log(foo % 2); //取餘:1

布爾值

布爾值包括truefalse。你可以給變數賦值為truefalse,並對其進行布爾操作。例如下列代碼:

var foo = true;
console.log(foo); //true

//常見的布爾操作符號: &&,||, !
console.log(true && true); //true
console.log(true && false); /false
console.log(true || false); //true
console.log(false || false); //false
console.log(!true); //false
console.log(!false); //true

數組

在Javascript中,我們可以通過[]創建數組。數組對象包含很多有用的函數,例如下列代碼所示:

var foo = [];

foo.push(1); //添加到數組末尾
console.log(foo); // [1]

foo.unshift(2);  //添加到數組頭部
console.log(foo); // [2, 1]

//數組起始位置從0開始
console.log(foo[0]); // 2

對象字面量

Javascript中通常使用對象字面量{}創建對象,例如下列代碼所示:

var foo = {};
console.log(foo); // {}
foo.bar = 123;
console.log(foo); // {bar: 123}

上面的代碼在運行時添加對象屬性,我們也可以在創建對象時定義對象屬性:

var foo = {
  bar: 123
};
console.log(foo); // {bar: 123}

對象字面量中可以嵌套其它對象字面量,例如下列代碼所示:

var foo = {
  bar: 123,
  bas: {
    bas1: 'some string',
    bas2: 345
  }
};
console.log(foo);

當然,對象字面量中也可以包含數組:

var foo = {
  bar: 123,
  bas: [1,2,3]
};
console.log(foo);

數組當中也可以包含對象字面量:

var foo = {
  bar: 123,
  bas: [{
      qux: 1
    },
    {
      qux: 2
    },
    {
      qux: 3
    }]
};
console.log(foo.bar); //123
console.log(foo.bas[0].qux); // 1
console.log(foo.bas[2].qux); // 2

函數

Javascript的函數非常強大,我們接下來將通過一系列的例子來逐漸瞭解它。
通常情況下的Javascript函數結構如下所示:

function functionName(){
  //函數體
}

Javascript的所有函數都有返回值。在沒有顯式聲明返回語句的情況下,函數會返回undefined。例如下麵代碼所示:

function foo(){return 123;}
console.log(foo); // 123

function bar(){ }
console.log(bar()); // undefined

立即執行函數

我們在定義函數以後立即執行它,通過括弧()包裹並調用函數。如下列代碼所示:

(function foo(){
  console.log('foo was executed!');
})();

出現立即執行函數的原因是為了創建新的變數作用域。ifelsewhile不會創建新的變數作用域,如下列代碼所示:

var foo = 123;
if(true){
  var foo = 456;
}
console.log(foo); // 456

在Javascrit中,我們通過函數創建新的變數作用域,例如使用立即執行函數:

var foo = 123;
if(true){
  (function(){
      var foo = 456;
  })();
}
console.log(foo); // 123

在上面的代碼中,我們沒有給函數命名,這被稱為匿名函數。

匿名函數

沒有名字的函數被稱為匿名函數。在Javascript中,我們可以把函數賦值給變數,如果準備將函數當作變數使用,就不需要給函數命名。下麵給出了兩種等價的寫法:

var foo1 = function nameFunction(){
  console.log('foo1');
}
foo1(); // foo1

var foo2 = function(){
  console.log('foo2');
}
foo2(); // foo2f

據說如果一門編程語言能夠把函數當作變數來對待,它就是一門優秀的編程語言,Javascript做到了這一點。

高階函數

由於Javascript允許我們將函數賦值給變數,所以我們可以將函數作為參數傳遞給其它函數。將函數作為參數的函數被稱為高階函數。setTimeout就是常見的高階函數。

setTimeout(function(){
    console.log('2000 milliseconds have passed since this demo started');
}, 2000);

如果在Node.js中運行上面的代碼,會看到命令視窗2秒鐘後輸出信息。在上面的代碼中,我們傳遞了一個匿名函數作為setTimeout的第一個參數。我們也可以傳遞一個普通的函數:

function foo(){
  console.log('2000 milliseconds have passed since this demo started');
}
setTimeout(foo, 200);

現在,我們已經瞭解了對象字面量和函數,接下來我們會瞭解閉包的概念。

閉包

閉包是能夠訪問其它函數內部變數的函數。如果在函數內部定義另一個函數,內部函數能夠訪問外部函數的變數,這就是閉包的常見形式。我們會通過一些例子來解釋。
在下麵的代碼中,你可以看到內部函數能夠訪問外部函數的變數:

function outerFunction(arg){
  var variableInOuterFunction = arg;
  function bar(){
    console.log(variableInOuterFunction);
  }

  bar();
}

outerFunction('hello closure!');   // hello closure!

令人驚喜的是:內部函數在外部函數返回之後依然可以訪問外部函數作用域中的變數。這是因為,變數仍然被綁定於內部函數,不依賴於外部函數。例如:

function outerFunction(arg){
  var variableInOuterFunction = arg;
  return function(){
    console.log(variableInOuterFunction);
  }
}

var innerFunction = outerFunction('hello closure!');

innerFunction(); // hello closure!

現在,我們已經瞭解了閉包,接下來,我們會探究一下使Javascript成為一門適合伺服器端編程的語言的原因。

Node.js性能

Node.js致力於開發高性能應用程式。接下來的部分,我們會介紹大規模I/O問題,並分別展示傳統方式及Node.js是如何解決這個問題的。

大規模I/O問題

大多數Web應用通過硬碟或者網路(例如查詢另一臺機器的資料庫)獲取數據,從硬碟或網路獲取數據的速度遠遠慢於CPU的處理周期。當收到一個HTTP請求以後,我們需要從資料庫獲取數據,請求會一直等待直到獲取數據完成。這些創建的連接和還未結束的請求會消耗伺服器的資源(記憶體和CPU)。為了使同一臺Web伺服器能夠處理大規模請求,我們需要解決大規模I/O問題。

每一個請求創建一個進程

傳統的Web伺服器為每一個請求創建一個新的進程,這是一種對記憶體和CPU開銷都很昂貴的操作。PHP最開始就是採用的這種方法。在等待響應期間,進程仍然會消耗資源,並且進程的創建更慢。所以現代Web應用大多使用線程池的方法。

線程池

現代Web伺服器使用線程池來處理每個請求。線程和進程相比,更加輕量級。在創建線程池以後,我們就不再需要為開始或結束進程而付出額外代價。當收到一個請求,我們為它分配一個線程。然而,線程池仍然會浪費一些資源。

單線程模式

我們知道為請求分別創建進程或者線程會導致系統資源浪費。與之相對,Node.js採取了單線程來處理請求。單線程伺服器的性能優於線程池伺服器的理念並不是Node.js首創,Nginx也是基於這種理念。Nginx是一種單線程伺服器,能夠處理極大數量的併發請求。
Javascript是單線程的,如果你有一個耗時操作(例如網路請求),就必須使用回調。下麵的代碼使用setTimeout模擬了一個耗時操作,可以用Node.js執行。

function longRunningOperation(callback){
  setTimeout(callback, 3000);
}
function UserClicked(){
  console.log('starting a long operation');
  longRunningOperation(function(){
      console.log('ending a long operation');
  })
}
UserClicked();

讓我們模擬一下Web請求:

function longRunningOperation(callback){
  setTimeout(callback, 3000);
}
function webRequest(request){
  console.log('starting a long operation for request:', request.id);
  longRunningOperation(function(){
    console.log('ending a long operation for request:', request.id);
  });
}

webRequest({id: 1});
webRequest({id: 2});

//輸出
//starting a long operation for request: 1
//starting a long operation for request: 2
//ending a long operation for request: 1
//ending a long operation for request: 2

更多的Node.js細節

Node.js的核心是一個event loopevent loop使得任何用戶圖形界面應用程式可以在任何操作系統中工作。當事件被觸發時(例如:用戶點擊滑鼠),操作系統調用程式的某個函數,程式執行函數中的代碼。之後,程式準備響應已經在隊列中的事件或尚未出現的事件。

線程饑餓

通常,在GUI程式中,當由一個事件調用的函數執行期間,其它事件不會被處理。因此,當你在相關函數中執行耗時操作時,GUI會變得無響應。這種CPU資源的短缺被成為饑餓
Node.js基於和GUI應用程式相同的event loop原則。因此,它也會面臨饑餓的問題。為了幫助更好的理解,我們通過幾個例子來說明:

console.time('timer');
setTimeout(function(){
  console.timeEnd('timer');  //timer: 1002.615ms
}, 1000)

運行這段代碼,與我們期望的相同,終端顯示的數字在1000ms左右。
接下來我們想寫一段耗時更長的代碼,例如一個未經優化的計算Fibonacci數列的方法:

console.time('timeit');
function fibonacci(n){
  if(n<2){
    return 1;
  }else{
    return fibonacci(n-2) + fibonacci(n-1);
  }
}
fibonacci(44);
console.timeEnd('timeit');  //我的電腦耗時 11863.331ms,每臺電腦會有差異

現在我們可以模擬Node.js的線程饑餓。setTimeout用於在指定的時間以後調用函數,如果我們在函數調用以前,執行一個耗時方法,由於耗時方法占用CPU和Javascript線程,setTimeout指定的函數無法被及時調用,只能等待耗時方法運行結束以後被調用。例如下麵的代碼:

function fibonacci(n){
  if(n<2){
    return 1;
  }else{
    return fibonacci(n-2) + fibonacci(n-1);
  }
}
console.time('timer');
setTimeout(function(){
  console.timeEnd('timer');  // 輸出時間會大於 1000ms
}, 1000)

fibonacci(44);

所以,如果你面臨CPU密集型場景,Node.js並不是最佳選擇,但也很難找到其它合適的平臺。但是Node.js非常適用於I/O密集型場景。

數據密集型應用

Node.js適用於I/O密集型。單線程機制意味著Node.js作為Web伺服器會占用更少的記憶體,能夠支持更多的請求。與執行代碼相比,從資料庫獲取數據需要花費更多的時間。下圖展示了傳統的線程池模型的伺服器是如何處理用戶請求的:


Node.js伺服器處理請求的方式如下圖。因為所有的工作都在單線程內完成,所以消耗更少的記憶體,同時因為不需要切換線程,所以CPU負載更小。

V8 Javascript引擎

Node.js中的所有Javascript通過V8 Javascript引擎執行。V8產生於谷歌Chrome項目,V8在Chrome中用於運行Javascript。V8不僅速度更快,而且很容易被集成到其它項目。

更多的Javascript

精通Javascript使得Node.js開發者不僅能夠寫出更加容易維護的項目,而且能夠利用到Javascript生態鏈的優勢。

預設值

Javascript變數的預設值是undefined。如下列代碼所示:

var foo;
console.log(foo);  //undefined

變數不存在的屬性也會返回undefined

var foo = {bar: 123};
console.log(foo.bar); // 123
console.log(foo.bas); // undefined

全等

需要註意Javascript當中 =====的區別。==會對變數進行類型轉換,===不會。推薦的用法是總是使用===

console.log(5 == '5'); // true
console.log(5 === '5'); // false

null

null是一個特殊的Javascript對象,用於表示空對象。而undefined用於表示變數不存在或未初始化。我們不需要給變數賦值為undefined,因為undefined是變數的預設值。

透露模塊模式

透露模塊模式的關鍵在於Javascript對閉包的支持以及能夠返回任意對象的能力。如下列代碼所示:

function printableMessage(){
  var message = 'hello';
  function setMessage(newMessage){
    if(!newMessage) throw new Error('cannot set empty message');
    message = newMessage;
  }
  function getMessage(){
    return message;
  }
  function printMessage(){
    console.log(message);
  }
  return {
    setMessage: setMessage,
    getMessage: getMessage,
    printMessage: printMessage
  };
}

var awesome1 = printableMessage();
awesome1.printMessage(); //hello

var awesome2 = printableMessage();
awesome2.setMessage('hi');
awesome2.printMessage(); // hi

awesome1.printMessage(); //hello

理解this

this總是指向調用函數的對象。例如:

var foo = {
  bar: 123,
  bas: function(){
    console.log('inside this.bar is: ', this.bar);
  }
}
console.log('foo.bar is:', foo.bar); //foo.bar is: 123
foo.bas(); //inside this.bar is: 123

由於函數basfoo對象調用,所以this指向foo。如果是純粹的函數調用,則this指向全局變數。例如:

function foo(){
  console.log('is this called from globals? : ', this === global); //true
}
foo();

如果我們在瀏覽器中執行上面的代碼,全局變數global會變為window
如果函數的調用對象改變,this的指向也會改變:

var foo = {
  bar: 123
};
function bas(){
  if(this === global){
    console.log('called from global');
  }
  if(this === foo){
    console.log('called from foo');
  }
}
//指向global
bas(); //called from global
//指向foo
foo.bas = bas;
foo.bas(); //called from foo

如果通過new操作符調用函數,函數內的this會指向由new創建的對象。

function foo(){
  this.foo = 123;
  console.log('Is this global? : ', this == global);
}

foo(); // Is this global? : true
console.log(global.foo); //123

var newFoo = new foo(); //Is this glocal ? : false
console.log(newFoo.foo); //123

通過上面代碼,我們可以看到,在通過new調用函數時,函數內的this指向發生改變。

理解原型

Javascript通過new操作符及原型屬性可以模仿面向對象的語言。每個Javascript對象都有一個被稱為原型的內部鏈接指向其他對象。
當我們調用一個對象的屬性,例如:foo.bar,Javascript會檢查foo對象是否存在bar屬性,如果不存在,Javascript會檢查bar屬性是否存在於foo._proto_,以此類推,直到對象不存在_proto_。如果在任何層級發現屬性的值,則立即返回,否則,返回undefined

var foo ={};
foo._proto_.bar = 123;
console.log(foo.bar); //123

當我們通過new操作符創建對象時,對象的_proto_會被賦值為函數的prototype屬性,例如:

function foo(){};
foo.prototype.bar = 123;

var bas = new foo();
console.log(bas._proto_ === foo.prototype); //true
console.log(bas.bar);

函數的所有實例共用相同的prototype

function foo(){};
foo.prototype.bar = 123;

var bas = new foo();
var qux = new foo();
console.log(bas.bar); //123
console.log(qux.bar); //123

foo.prototype.bar = 456;
console.log(bas.bar); //456
console.log(qux.bar); //456

只有當屬性不存在時,才會訪問原型,如果屬性存在,則不會訪問原型。

function foo(){};
foo.prototype.bar = 123;

var bas = new foo();
var qux = new foo();

bas.bar = 456;
console.log(bas.bar);//456

console.log(qux.bar); //123

上面的代碼表明,如果修改了bas.barbas._proto_.bar就不再被訪問。

錯誤處理

Javascript的異常處理機制類似其它語言,通過throw關鍵字拋出異常,通過catch關鍵字捕獲異常。例如:

try{
  console.log('About to throw an error');
  throw new Error('Error thrown');
}
catch(e){
  console.log('I will only execute if an error is thrown');
  console.log('Error caught: ', e.message);
}
finally{
  console.log('I will execute irrespective of an error thrown');
}

總結

本章,我們介紹了一些Node.js及Javascript的重要概念,知道了Node.js適用於開發數據密集型應用程式。下章我們將開始介紹如何使用Node.js開發應用程式。

學習過程中遇到什麼問題或者想獲取學習資源的話,歡迎加入學習交流群
343599877,我們一起學前端!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、縱向菜單 效果圖: 二、橫向菜單 效果圖: 三、下拉菜單 效果圖: 縱向的下拉菜單 給最外層容器nav添加額外的一個類,vertical成為<nav id="nav1" class="vertical">。。。。<nav> 然後添加如下的css規則: 效果圖: ...
  • 製作網站時,可能會用到視覺差效果 如圖 視覺差在製作網頁時會有很炫酷的效果,今天要講的是如何呈現動態視覺差 效果如圖: 製作方法首先需要一個視覺差的插件 我所用的插件是一款較為大眾的視覺差插件 導入這三個css文件後,在style中可以修改圖片 將圖片修改為指定gif即可 下麵介紹不使用插件方法 選 ...
  • <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>放大鏡</title> <style> *{ margin:0px; padding:0px; } #box{ width:430px; height:430px; ...
  • <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>Bootstrap</tit ...
  • 在a.less中導入base.lessa.less中的代碼 base.less中的代碼 @value的值為[ripple, .5s, linear]"@{value}"成為字元串"[ripple, .5s, linear]",避免js解析錯誤。~避免編譯,不加則輸出為"ripple"Tips: 少一 ...
  • 之前對事件模型還是比較清楚的,許多概念都清晰映射在腦海中。工作之後,一方面使用的 局限性,二是習慣於用框架中的各種事件監聽方式,簡單即方便,久而久之,事件的一些概念開 始淡出記憶中,就像我現在已經開始淡忘C語言的指針、麥克斯韋方程組、矩陣的變換、最小二乘 法等。知識就像五彩繽紛的鵝卵石鋪墊在你前行的 ...
  • 基礎框架 瞭解HTML的代碼註釋 什麼是代碼註釋?代碼註釋的作用是幫助程式員標註代碼的用途,過一段時間後再看你所編寫的代碼,就能很快想起這段代碼的用途。代碼註釋不僅方便程式員自己回憶起以前代碼的用途,還可以幫助其他程式員很快的讀懂你的程式的功能,方便多人合作開髮網頁代碼。 語法: <!--註釋文字 ...
  • 上手簡單,讓Atom能夠成為了一款優雅而低門檻的神器,深度可定製的特性,讓Atom在你的打磨之下,變得越來越符合你的心意 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...