1. 程式人生 > >node事件佇列

node事件佇列

執行棧中的程式碼(同步任務),總是在讀取"任務佇列"(非同步任務)之前執行。

var req = new XMLHttpRequest(); req.open('GET', url); req.onload = function (){}; req.onerror = function (){}; req.send();

它與下面的寫法等價
var req = new XMLHttpRequest(); req.open('GET', url); req.send(); req.onload = function (){}; req.onerror = function (){}; 
因為req.send方法是Ajax操作向伺服器傳送資料,它是一個非同步任務;而指定回撥函式的部分(onload和onerror),在send()方法的前面或後面無關緊要,因為它們屬於執行棧的一部分,系統總是執行完它們,才會去讀取"任務佇列"。


下面程式碼的執行結果
setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); process.nextTick(() => console.log(3)); Promise.resolve().then(() => console.log(4)); (() => console.log(5))();


5
3
4
1
2


非同步任務可以分成兩種,本輪迴圈一定早於次輪迴圈執行;
Node 規定,process.nextTickPromise的回撥函式,追加在本輪迴圈,即同步任務一旦執行完成,就開始執行它們。而setTimeout
setIntervalsetImmediate的回撥函式,追加在次輪迴圈。
process.nextTick是在本輪迴圈執行的,而且是所有非同步任務裡面最快執行的。
Promise物件的回撥函式,會進入非同步任務裡面的"微任務"(microtask)佇列,微任務佇列追加在process.nextTick佇列的後面,也屬於本輪迴圈。

本輪迴圈的執行順序
  1. 同步任務
  2. process.nextTick()
  3. 微任務

 

事件迴圈的初始化

  • 同步任務
  • 發出非同步請求
  • 規劃定時器生效的時間
  • 執行process.nextTick()等等

上面這些事情都幹完了,事件迴圈就正式開始了

  1. timers
    1. 處理setTimeout()setInterval()的回撥函式。
  2. I/O callbacks
    1. 執行除了setTimeout、setInterval、setImmediate、關閉請求外的其他回撥函式
  3. poll
    1. 等待還未返回的 I/O 事件,比如伺服器的迴應、使用者移動滑鼠等等
  4. check
    1. 執行setImmediate()的回撥
  5. close callbacks
    1. 執行關閉請求的回撥函式

 

// 非同步任務一:100ms 後執行的定時器
setTimeout(() => { const delay = Date.now() - timeoutScheduled; console.log(`${delay}ms`); }, 100);  // 非同步任務二:檔案讀取後,有一個 200ms 的回撥函式 fs.readFile('test.js', () => { const startCallback = Date.now(); while (Date.now() - startCallback < 200) {  // 什麼也不做 } });


第一輪事件迴圈以後,沒有到期的定時器,也沒有已經可以執行的 I/O 回撥函式,所以會進入 Poll 階段,等待核心返回檔案讀取的結果。由於讀取小檔案一般不會超過 100ms,所以在定時器到期之前,Poll 階段就會得到結果,因此就會繼續往下執行。
第二輪事件迴圈,依然沒有到期的定時器,但是已經有了可以執行的 I/O 回撥函式,所以會進入 I/O callbacks 階段,執行fs.readFile的回撥函式。這個回撥函式需要 200ms,也就是說,在它執行到一半的時候,100ms 的定時器就會到期。但是,必須等到這個回撥函式執行完,才會離開這個階段。
第三輪事件迴圈,已經有了到期的定時器,所以會在 timers 階段執行定時器。最後輸出結果大概是200多毫秒。

setTimeout在 timers 階段執行,而setImmediate在 check 階段執行。所以,setTimeout會早於setImmediate完成。
setTimeout(() => console.log(1)); setImmediate(() => console.log(2));
但是實際執行的時候,結果卻是不確定,有時還會先輸出2,再輸出1
因為setTimeout的第二個引數預設為0。但是實際上,Node 做不到0毫秒,最少也需要1毫秒;
進入事件迴圈以後,有可能到了1毫秒,也可能還沒到1毫秒,取決於系統當時的狀況。如果沒到1毫秒,那麼 timers 階段就會跳過,進入 check 階段,先執行setImmediate的回撥函式。

fs.readFile('test.js', () => { setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); });

一定是先輸出2,再輸出1。先進入 I/O callbacks 階段,然後是 check 階段,最後才是 timers 階段。因此,setImmediate才會早於setTimeout執行。


process.nextTick(function A() { console.log(1); process.nextTick(function B(){console.log(2);}); }); setTimeout(function timeout() { console.log('TIMEOUT FIRED'); }, 0)

// 1
// 2
// TIMEOUT FIRED
如果有多個process.nextTick語句(不管它們是否巢狀),將全部在當前"執行棧"執行。
 
setImmediate(function (){ setImmediate(function A() { console.log(1); setImmediate(function B(){console.log(2);}); }); setTimeout(function timeout() { console.log('TIMEOUT FIRED'); }, 0); }); // 1 // TIMEOUT FIRED // 2

因為setImmediate總是將事件註冊到下一輪Event Loop,所以函式A和timeout是在同一輪Loop執行,而函式B在下一輪Loop執行。
process.nextTick和setImmediate的一個重要區別:多個process.nextTick語句總是在當前"執行棧"一次執行完,多個setImmediate可能則需要多次loop才能執行完。事實上,這正是Node.js 10.0版新增setImmediate方法的原因,否則像下面這樣的遞迴呼叫process.nextTick,將會沒完沒了,主執行緒根本不會去讀取"事件佇列"!