node事件佇列
阿新 • • 發佈:2018-12-08
執行棧中的程式碼(同步任務),總是在讀取"任務佇列"(非同步任務)之前執行。
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.nextTick
和Promise
的回撥函式,追加在本輪迴圈,即同步任務一旦執行完成,就開始執行它們。而setTimeout
、setInterval
、setImmediate
的回撥函式,追加在次輪迴圈。
process.nextTick是在本輪迴圈執行的,而且是所有非同步任務裡面最快執行的。Promise
物件的回撥函式,會進入非同步任務裡面的"微任務"(microtask)佇列,微任務佇列追加在process.nextTick
佇列的後面,也屬於本輪迴圈。
本輪迴圈的執行順序
- 同步任務
- process.nextTick()
- 微任務
事件迴圈的初始化
- 同步任務
- 發出非同步請求
- 規劃定時器生效的時間
- 執行
process.nextTick()
等等
上面這些事情都幹完了,事件迴圈就正式開始了
- timers
- 處理
setTimeout()
和setInterval()
的回撥函式。
- 處理
- I/O callbacks
- 執行除了setTimeout、setInterval、setImmediate、關閉請求外的其他回撥函式
- poll
- 等待還未返回的 I/O 事件,比如伺服器的迴應、使用者移動滑鼠等等
- check
- 執行
setImmediate()
的回撥
- 執行
- close callbacks
- 執行關閉請求的回撥函式
// 非同步任務一: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,將會沒完沒了,主執行緒根本不會去讀取"事件佇列"!