1. 程式人生 > >方便大家學習的Node.js教程(一):理解Node.js

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

圖形 -1 iter pri attribute set run 相對 mage

理解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.bar, bas._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,我們一起學前端!

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