文章目錄
- 使用setTimeout延遲函數執行
- 使用clearTimeout取消執行計畫
- 制定和取消函數的重複執行計畫
- 使用process.nextTick將函數執行延遲到事件迴圈的下一輪
- 堵塞事件迴圈
- 退出事件迴圈
- 使用setTimeout替代setInterval來確保函數執行的串列性
本系列文章列表和翻譯進度,請移步:Node.js進階編程:用Javascript構建可伸縮應用(〇)
本文對應原文第二部分第六章:Node Core API Basics:Scheduling the Execution of Functions Using Timers
文章從Word複製到這裡,排版與原文不太一致,可以點這裡下載本文的PDF版。
第六章:使用計時器制定函數的執行計畫本章內容:
- 函數的順延強制
- 取消執行計畫
- 制定函數的周期性執行計畫
- 將函數執行延遲到事件迴圈的下一輪
如果你熟悉用戶端JavaScript編程,你可能使用過setTimeout和setInterval函數,這兩個函數允許延時一段時間再運行函數。比如下面的代碼, 一旦被載入到Web頁面,1秒後會在頁面文檔後追加“Hello there”:
var oneSecond = 1000 * 1; // one second = 1000 x 1 ms
setTimeout(function() {
document.write('<p>Hello there.</p>');
}, oneSecond);
而setInterval允許以指定的時間間隔重複執行函數。如果把下面的代碼注入到Web頁面,會導致每秒鐘向頁面文檔後面追加一句“Hello there”:
var oneSecond = 1000 * 1; // one second = 1000 x 1 ms
setInterval(function() {
document.write('<p>Hello there.</p>');
}, oneSecond);
因為Web早已成為一個用來構建應用程式的平台,而不再是簡單的靜態頁面,所以這種類似的需求日益浮現。這些任務計劃函數協助開發人員實現表單定期驗證,延遲遠端資料同步,或者那些需要延時反應的UI互動。Node也完整實現了這些方法。在伺服器端,你可以用它們來重複或順延強制很多任務,比如緩衝到期,串連池清理,會話到期,輪詢等等。
使用setTimeout延遲函數執行
setTimeout可以制定一個在將來某個時間把指定函數運行一次的執行計畫,比如:
var timeout_ms = 2000; // 2 seconds
var timeout = setTimeout(function() {
console.log("timed out!");
}, timeout_ms);
和用戶端JavaScript完全一樣,setTimeout接受兩個參數,第一個參數是需要被延遲的函數,第二個參數是延遲時間(以毫秒為單位)。
setTimeout返回一個逾時控制代碼,它是個內部對象,可以用它作為參數調用clearTimeout來取消計時器,除此之外這個控制代碼沒有任何作用。
使用clearTimeout取消執行計畫
一旦獲得了逾時控制代碼,就可以用clearTimeout來取消函數執行計畫,像這樣:
var timeoutTime = 1000; // one second
var timeout = setTimeout(function() {
console.log("timed out!");
}, timeoutTime);
clearTimeout(timeout);
這個例子裡,計時器永遠不會被觸發,也不會輸出”time out!”這幾個字。你也可以在將來的任何時間取消執行計畫,就像下面的例子:
var timeout = setTimeout(function A() {
console.log("timed out!");
}, 2000);
setTimeout(function B() {
clearTimeout(timeout);
}, 1000);
代碼指定了兩個延時執行的函數A和B,函數A計劃在2秒鐘後執行,B計劃在1秒鐘後執行,因為函數B先執行,而它取消了A的執行計畫,因此A永遠不會運行。
制定和取消函數的重複執行計畫
setInterval和setTimeout類似,但是它會以指定時間為間隔重複執行一個函數。你可以用它來周期性的觸發一段程式,來完成一些類似清理,收集,日誌,擷取資料,輪詢等其它需要重複執行的任務。
下面代碼每秒會向控制台輸出一句“tick”:
var period = 1000; // 1 second
setInterval(function() {
console.log("tick");
}, period);
如果你不想讓它永遠運行下去,可以用clearInterval()取消定時器。
setInterval返回一個執行計畫控制代碼,可以把它用作clearInterval的參數來取消執行計畫:
var interval = setInterval(function() {
console.log("tick");
}, 1000);
// …
clearInterval(interval);
使用process.nextTick將函數執行延遲到事件迴圈的下一輪
有時候用戶端JavaScript程式員用setTimeout(callback,0)將任務延遲一段很短的時間,第二個參數是0毫秒,它告訴JavaScript運行時,當所有掛起的事件處理完畢後立刻執行這個回呼函數。有時候這種技術被用來順延強制一些並不需要被立刻執行的操作。比如,有時候需要在使用者事件處理完畢後再開始播放動畫或者做一些其它的計算。
Node中,就像 “事件迴圈”的字面意思,事件迴圈運行在一個處理事件隊列的迴圈裡,事件迴圈工作過程中的每一輪就稱為一個tick。
你可以在事件迴圈每次開始下一輪(下一個tick)執行時調用回呼函數一次,這也正是process.nextTick的原理,而setTimeout,setTimeout使用JavaScript運行時內部的執行隊列,而不是使用事件迴圈。
通過使用process.nextTick(callback) ,而不是setTimeout(callback, 0),你的回呼函數會在隊列內的事件處理完畢後立刻執行,它要比JavaScript的逾時隊列快很多(以CPU時間來衡量)。
你可以像下面這樣,把函數延遲到下一輪事件迴圈再運行:
process.nextTick(function() {
my_expensive_computation_function();
});
注意:process對象是Node為數不多的全域對象之一。
堵塞事件迴圈
Node和JavaScript的運行時採用的是單線程事件迴圈,每次迴圈,運行時通過調用相關回呼函數來處理隊列內的下個事件。當事件執行完畢,事件迴圈取得執行結果並處理下個事件,如此反覆,直到事件隊列為空白。如果其中一個回呼函數運行時佔用了很長時間,事件迴圈在那期間就不能處理其它掛起的事件,這會讓應用程式或服務變得非常慢。
在處理事件時,如果使用了記憶體敏感或者處理器敏感的函數,會導致事件迴圈變得緩慢,而且造成大量事件堆積,不能被及時處理,甚至堵塞隊列。
看下面堵塞事件迴圈的例子:
process.nextTick(function nextTick1() {
var a = 0;
while(true) {
a ++;
}
});
process.nextTick(function nextTick2() {
console.log("next tick");
});
setTimeout(function timeout() {
console.log("timeout");
}, 1000);
這個例子裡,nextTick2和timeout函數無論等待多久都沒機會運行,因為事件迴圈被nextTick函數裡的無限迴圈堵塞了,即使timeout函數被計劃在1秒鐘後執行它也不會運行。
當使用setTimeout時,回呼函數會被添加到執行計畫隊列,而在這個例子裡它們甚至不會被添加到隊列。這雖然是個極端例子,但是你可以看到,運行一個處理器敏感的任務時可能會堵塞或者拖慢事件迴圈。
退出事件迴圈
使用process.nextTick,可以把一個非關鍵性的任務延遲到事件迴圈的下一輪(tick)再執行,這樣可以釋放事件迴圈,讓它可以繼續執行其它掛起的事件。
看下面例子,如果你打算刪除一個臨時檔案,但是又不想讓data事件的回呼函數等待這個IO操作,你可以這樣延遲它:
stream.on("data", function(data) {
stream.end("my response");
process.nextTick(function() {
fs.unlink("/path/to/file");
});
});
使用setTimeout替代setInterval來確保函數執行的串列性
假設,你打算設計一個叫my_async_function的函數,它可以做某些I/O操作(比如解析記錄檔)的函數,並打算讓它周期性執行,你可以用setInterval這樣實現它:
var interval = 1000;
setInterval(function() {
my_async_function(function() {
console.log('my_async_function finished!');
});
},interval);//譯者註:前面加粗部分是我添加的,作者應該是筆誤遺漏了
你必須能確保這些函數不會被同時執行,但是如果使用setinterval你無法保證這一點,假如my_async_function函數啟動並執行時間比interval變數多了一毫秒,它們就會被同時執行,而不是按次序串列執行。
譯者註:(下面粗體部分為譯者添加,非原書內容)
為了方便理解這部分內容,可以修改下作者的代碼,讓它可以實際運行:
var interval = 1000;
setInterval(function(){
(function my_async_function(){
setTimeout(function(){
console.log("1");
},5000);
})();
},interval);
運行下這段代碼看看,你會發現,等待5秒鐘後,“hello ”被每隔1秒輸出一次。而我們期望是,當前my_async_function執行完畢(耗費5秒)後,等待1秒再執行下一個my_async_function,每次輸出之間應該間隔6秒才對。造成這種結果,是因為my_async_function不是串列執行的,而是多個在同時運行。
因此,你需要一種辦法來強制使一個my_async_function執行結束到下個my_async_function開始執行之間的間隔時間正好是interval變數指定的時間。你可以這樣做:
var interval = 1000; // 1 秒
(function schedule() { //第3行
setTimeout(function do_it() {
my_async_function(function() { //第5行
console.log('async is done!');
schedule();
});
}, interval);
}()); //第10行
前面代碼裡,聲明了一個叫schedule的函數(第3行),並且在聲明後立刻調用它(第10行),schedule函數會在1秒(由interval指定)後運行do_it函數。1秒鐘過後,第5行的my_async_function函數會被調用,當它執行完畢後,會調用它自己的那個匿名回呼函數(第6行),而這個匿名回呼函數又會再次重設do_it的執行計畫,讓它1秒鐘後重新執行,這樣代碼就開始串列地不斷迴圈執行了。
小結
可以用setTimeout()函數預先設定函數的執行計畫,並用clearTimeout()函數取消它。還可以用setInterval()周期性的重複執行某個函數,相應的,可以使用clearInterval()取消這個重複執行計畫。
如果因為使用了一個處理器敏感的操作而堵塞了事件迴圈,那些原計劃應該被執行的函數將會被延遲,甚至永遠無法執行。所以不要在事件迴圈內使用CPU敏感的操作。還有,你可以使用process.nextTick()把函數的執行延遲到事件迴圈的下一輪。
I/O和setInterval()一起使用時,你無法保證在任何時間點只有一個掛起的調用,但是,你可以使用遞迴函式和setTimeout()函數來迴避這個棘手的問題。