理解 Node.js process.nextTick() {Understanding process.nextTick()}

有很多人對Node.js裏process.nextTick()的用法感到不理解,下面我們就來看一下process.nextTick()到底是什麼,該如何使用。

Node.js是單線程的,除了系統IO之外,在它的事件輪詢過程中,同一時間只會處理一個事件。你可以把事件輪詢想象成一個大的隊列,在每個時間點上,系統只會處理一個事件。即使你的電腦有多個CPU核心,你也無法同時並行的處理多個事件。但也就是這種特性使得node.js適合處理I/O型的應用,不適合那種CPU運算型的應用。在每個I/O型的應用中,你只需要給每一個輸入輸出定義一個回調函數即可,他們會自動加入到事件輪詢的處理隊列裏。當I/O操作完成後,這個回調函數會被觸發。然後系統會繼續處理其他的請求。

在這種處理模式下,process.nextTick()的意思就是定義出一個動作,並且讓這個動作在下一個事件輪詢的時間點上執行。我們來看一個例子。例子中有一個foo(),你想在下一個時間點上調用他,可以這麼做:

1 function foo() {
2     console.error('foo');
3 }
4  
5 process.nextTick(foo);
6 console.error('bar');
運行上面的代碼,你從下面終端打印的信息會看到,"bar"的輸出在“foo”的前面。這就驗證了上面的說法,foo()是在下一個時間點運行的。
1 bar
2 foo
你也可以使用setTimeout()函數來達到貌似同樣的執行效果:
1 setTimeout(foo, 0);
2 console.log('bar');
但在內部的處理機制上,process.nextTick()和setTimeout(fn, 0)是不同的,process.nextTick()不是一個單純的延時,他有更多的 特性

更精確的說,process.nextTick()定義的調用會創建一個新的子堆棧。在當前的棧裏,你可以執行任意多的操作。但一旦調用netxTick,函數就必須返回到父堆棧。然後事件輪詢機制又重新等待處理新的事件,如果發現nextTick的調用,就會創建一個新的棧。

下面我們來看看,什麼情況下使用process.nextTick():

在多個事件裏交叉執行CPU運算密集型的任務:

在下面的例子裏有一個compute(),我們希望這個函數儘可能持續的執行,來進行一些運算密集的任務。

但與此同時,我們還希望系統不要被這個函數堵塞住,還需要能響應處理別的事件。這個應用模式就像一個單線程的web服務server。在這裏我們就可以使用process.nextTick()來交叉執行compute()和正常的事件響應。

01 var http = require('http');
02  
03 function compute() {
04     // performs complicated calculations continuously
05     // ...
06     process.nextTick(compute);
07 }
08  
09 http.createServer(function(req, res) {
10      res.writeHead(200, {'Content-Type''text/plain'});
11      res.end('Hello World');
12 }).listen(5000, '127.0.0.1');
13  
14 compute();

在這種模式下,我們不需要遞歸的調用compute(),我們只需要在事件循環中使用process.nextTick()定義compute()在下一個時間點執行即可。在這個過程中,如果有新的http請求進來,事件循環機制會先處理新的請求,然後再調用compute()。反之,如果你把compute()放在一個遞歸調用裏,那系統就會一直阻塞在compute()裏,無法處理新的http請求了。你可以自己試試。

當然,我們無法通過process.nextTick()來獲得多CPU下並行執行的真正好處,這只是模擬同一個應用在CPU上分段執行而已。

保持回調函數異步執行的原則

當你給一個函數定義一個回調函數時,你要確保這個回調是被異步執行的。下面我們看一個例子,例子中的回調違反了這一原則:

1 function asyncFake(data, callback) {       
2     if(data === 'foo') callback(true);
3     else callback(false);
4 }
5  
6 asyncFake('bar'function(result) {
7     // this callback is actually called synchronously!
8 });
爲什麼這樣不好呢?我們來看Node.js 文檔裏一段代碼:
1 var client = net.connect(8124, function() {
2     console.log('client connected');
3     client.write('world!\r\n');
4 });

在上面的代碼裏,如果因爲某種原因,net.connect()變成同步執行的了,回調函數就會被立刻執行,因此回調函數寫到客戶端的變量就永遠不會被初始化了。

這種情況下我們就可以使用process.nextTick()把上面asyncFake()改成異步執行的:

1 function asyncReal(data, callback) {
2     process.nextTick(function() {
3         callback(data === 'foo');      
4     });
5 }

用在事件觸發過程中

來看一個例子,你想寫一個庫實現這樣的功能:從源文件裏讀取數據,當讀取完畢後,觸發一個事件同時傳遞讀取的數據。可能你會這樣寫:

1 var EventEmitter = require('events').EventEmitter;
2  
3 function StreamLibrary(resourceName) {
4     this.emit('start');
5  
6     // read from the file, and for every chunk read, do:       
7     this.emit('data', chunkRead);      
8 }
9 StreamLibrary.prototype.__proto__ = EventEmitter.prototype;   // inherit from EventEmitter
下面是一段調用這個庫的客戶端程序,我們想在程序中監聽這些事件:


1 var stream = new StreamLibrary('fooResource');
2  
3 stream.on('start'function() {
4     console.log('Reading has started');
5 });
6  
7 stream.on('data'function(chunk) {
8     console.log('Received: ' + chunk);
9 });

但是上面的代碼中,將永遠接收不到“start”事件,因爲在這個庫實例化的時候,“start”事件會被立刻觸發執行,但此時事件的回調函數還沒有準備好,所以在客戶端根本無法接收到這個事件。同樣,我們可以用process.nextTick()來改寫事件觸發的過程,下面是一個正確的版本:

01 function StreamLibrary(resourceName) {     
02     var self = this;
03  
04     process.nextTick(function() {
05         self.emit('start');
06     });
07  
08     // read from the file, and for every chunk read, do:       
09     this.emit('data', chunkRead);      
10 }

本文轉自:http://www.oschina.net/translate/understanding-process-next-tick?cmp
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章