再次理解是事件迴圈(Event Loop)
1. 事件迴圈
JavaScript 是單執行緒執行,非同步操作特別重要。
只要用到引擎之外的功能,就需要跟外部互動,從而形成非同步操作。由於非同步操作實在太多,JavaScript 不得不提供很多非同步語法。這就好比,有些人老是受打擊, 他的抗打擊能力必須變得很強,否則他就完蛋了。
Node 的非同步語法比瀏覽器更復雜,因為它可以跟核心對話,不得不搞了一個專門的庫 libuv 做這件事。這個庫負責各種回撥函式的執行時間,畢竟非同步任務最後還是要回到主執行緒,一個個排隊執行。
為了協調非同步任務,Node 居然提供了四個定時器,讓任務可以在指定的時間執行。
- setTimeout()
- setInterval()
- setImmediate()
- process.nextTick()
前兩個是語言的標準,`後兩個是 Node 獨有的。它們的寫法差不多,作用也差不多,不太容易區別
2.同步任務和非同步任務
同步任務即正常業務程式碼,不含回撥
// test.js setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); process.nextTick(() => console.log(3)); new promise(()=>{ console.log(4) }) Promise.resolve().then(() => console.log(5)); (() => console.log(6))(); /*結果 4 6 3 5 1 2 */
同步任務總比非同步任務任務先執行
上面程式碼中只有以下兩行程式碼是同步程式碼,所有最先執行,輸4
,6
new Promise(()=>{
console.log(4)
})
Promise.resolve().then(() => console.log(5));
(() => console.log(6))();
非同步任務可以分成兩種:
- 追加在本輪迴圈的非同步任務
- 追加在次輪迴圈的非同步任務
迴圈指的是事件迴圈(EventLoop),本輪迴圈一定比次輪迴圈先執行
Node 規定,process.nextTick
和Promise
的回撥函式,追加在本輪迴圈,同步任務執行結束完(優先順序process.nextTick>promise.then()
setTimeout
、setInterval
、setImmediate
的回撥函式,追加在次輪迴圈。
// 下面兩行,次輪迴圈執行
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
// 下面兩行,本輪迴圈執行
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(5));
3.process.nextTick()
雖然名字看起來像是次輪迴圈,實際上是追加在本次迴圈。
Node執行完所有的同步任務後,就會立馬執行process.nextTick
的任務佇列(nextTickQueue),所以下面這行程式碼是第三個輸出結果。基本上,如果你希望任務儘可能快的執行,那就使用process.nextTick()
process.nextTick(() => console.log(3));
4.微任務
根據語言規定,Promise
物件的回撥函式,會進入非同步任務裡面的“微任務佇列”(microtask)。
微任務佇列追加在process.nextTick
佇列後面。也屬於本輪迴圈,所以以下程式碼總是先輸出3
在輸出5
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(5));
// 3
// 5
注意:只有一個佇列趣步清空了以後,才會執行下一個佇列。
process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
// 1
// 3
// 2
// 4
上面程式碼中,全部process.nextTick()
的回撥函式,都會早於Promise
的。
綜上,本輪迴圈的執行順序為
- 1.同步任務
- 2.process.nextTick()
- 微任務
5.事件迴圈的概念
Node文件官方定義如下
When Node.js starts, it initializes the event loop, processes the provided input
script which may make async API calls, schedule timers, or call process.nextTick(),
then begins processing the event loop.
當Node.js啟動時,它將初始化事件迴圈,處理提供的輸入指令碼,該輸入指令碼可能進行
非同步API呼叫,排程計時器或呼叫process.nextTick(),然後開始處理事件迴圈。
這段話很重重要,首先,有的人認為,除了主執行緒,還存在一個單獨的事件迴圈執行緒。不是這樣的,只有一個主執行緒,事件迴圈是在主執行緒上完成的。其次,Node開始執行指令碼時,會先進行事件迴圈的初始化,但是此時事件迴圈還沒有開始,會先完成以下工作
- 同步任務
- 發出非同步請求
- 規定定時器生效的時間
- 執行process.nextTick()等等
最後,上面這些事情都幹完了,事件迴圈就開始了。
6.事件迴圈的六個階段
事件迴圈會無限次執行,一輪又一輪,只有非同步任務的回撥函式佇列清空了,才會停止執行。
每一輪的事件迴圈都分成六個階段,這些階段會依次執行
1. timers
2. I/O callbacks
3. idle, prepare
4. poll
5. check
6. close callbacks
每一個階段都有一個先進先出的回撥函式佇列,只有一個階段的回撥函式佇列清空了,該執行的回撥函式都執行了,事件迴圈才會進入下一個階段。
下面簡單介紹一下每個階段的含義,詳細介紹可以看官方文件,也可以參考 libuv 的原始碼解讀。
1. Timer
這個是定時器階段,處理setTimeOut()
和setInterval()
的回撥函式。進入這個階段後,主執行緒會檢查一下當前時間,是否滿足定時器的條件。如果滿足就執行回撥函式,否則就離開這個階段,進行下一階段
2. I/O callbacks
除了以下操作的的回撥函式,其他回撥函式都在這個階段執行。
setTimeOut()
和setInterval()
的回撥函式setImmediate()
的回撥函式- 用於關閉請求的回撥函式,比如
socket.on('close',...)
3. idle, prepare
該階段只供 libuv 內部呼叫,這裡可以忽略。
4. Poll
1)執行下限時間已經達到的timers的回撥,
2)然後處理 poll 佇列裡的事件。
當event loop進入 poll 階段,並且 沒有設定的 timers(there are no timers scheduled),會發生下面兩件事之一:
- 如果 poll 佇列不空,event loop會遍歷佇列並同步執行回撥,直到佇列清空或執行的回撥數到達系統上限;
- 如果 poll 佇列為空,則發生以下兩件事之一:
- 如果程式碼已經被setImmediate()設定了回撥, event loop將結束 poll 階段進入 check 階段來執行 check 佇列(裡面的回撥 callback)。
如果程式碼沒有被setImmediate()設定回撥,event loop將阻塞在該階段等待回撥被加入 poll 佇列,並立即執行。 - 但是,當event loop進入 poll 階段,並且 有設定的timers,一旦 poll 佇列為空(poll 階段空閒狀態):
event loop將檢查timers,如果有1個或多個timers的下限時間已經到達,event loop將繞回 timers 階段,並執行 timer 佇列。
- 如果程式碼已經被setImmediate()設定了回撥, event loop將結束 poll 階段進入 check 階段來執行 check 佇列(裡面的回撥 callback)。
5. check
該階段執行setImmediate()的回撥函式。
6. close callbacks
該階段執行關閉請求的回撥函式,比如socket.on('close', ...)。
7.事件迴圈的例子
const fs = require('fs');
const timeoutScheduled = Date.now();
// 非同步任務一:100ms 後執行的定時器
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms`);
}, 100);
// 非同步任務二:檔案讀取後,有一個 200ms 的回撥函式
fs.readFile('map.js', () => {
const startCallback = Date.now();
console.log(`read time: ${startCallback -timeoutScheduled}ms`)
while (Date.now() - startCallback < 200) {
// 什麼也不做
}
});
// read time: 2ms
// 202ms
上面程式碼有兩個非同步任務,一個是 100ms 後執行的定時器,一個是檔案讀取,它的回撥函式需要 200ms。請問執行結果是什麼?
指令碼進入第一輪事件迴圈以後,沒有到期的定時器,也沒有已經可以執行的 I/O 回撥函式,所以會進入 Poll 階段,等待核心返回檔案讀取的結果。由於讀取小檔案一般不會超過 100ms,所以在定時器到期之前,Poll 階段就會得到結果,因此就會繼續往下執行。
第二輪事件迴圈,依然沒有到期的定時器,但是已經有了可以執行的 I/O 回撥函式,所以會進入 I/O callbacks 階段,執行fs.readFile的回撥函式。這個回撥函式需要 200ms,也就是說,在它執行到一半的時候,100ms 的定時器就會到期。但是,必須等到這個回撥函式執行完,才會離開這個階段。
第三輪事件迴圈,已經有了到期的定時器,所以會在 timers 階段執行定時器。最後輸出結果大概是200多毫秒。
8. setTimeout 和 setImmediate
由於setTimeout
在 timers
階段執行,而setImmediate
在 check
階段執行。所以,setTimeout
會早於setImmediate
完成。
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
上面程式碼應該先輸出 1
,再輸出2
,但是實際執行的時候,結果卻是不確定
,有時還會先輸出2
,再輸出1
。
這是因為setTimeou
t的第二個引數預設為0
。但是實際上,Node 做不到0毫秒,最少
也需要1毫秒
,根據官方文件,第二個引數的取值範圍在1毫秒
到2147483647毫秒
(2^31 -1)之間。也就是說,setTimeout(f, 0)
等同於setTimeout(f, 1)
。
實際執行的時候,進入事件迴圈以後,有可能到了1毫秒,也可能還沒到1毫秒,取決於系統當時的狀況。如果沒到1毫秒
,那麼 timers 階段就會跳過
,進入 check 階段
,先執行setImmediate的回撥函式
。
但是,下面的程式碼一定是先輸出2,再輸出1。
const fs = require('fs');
fs.readFile('test.js', () => {
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
});
上面程式碼會先進入 I/O callbacks 階段
,然後是 check
階段,最後才是 timers
階段。因此,setImmediate才會早於setTimeout執行。
9.部分面試題
- process.nextTick + setImmediate
setImmediate(function () {
console.log(1);
process.nextTick(function () {
console.log(2);
});
});
process.nextTick(function () {
console.log(3);
setImmediate(function () {
console.log(4);
})
});
// 3 1 2 4
2
setTimeout(function () {
console.log(1)
Promise.resolve().then(() => console.log(3))
process.nextTick(() => console.log(4))
}, 0);
setImmediate(() => console.log(2))
// 1 4 3 2
這個是因為netxtTick任務見縫插針,每個階段完成都會查詢。promise.then只會每輪查詢。
3
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout0')
},0)
setTimeout(function(){
console.log('setTimeout3')
},3)
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
async1();
new Promise(function(resolve){
console.log('promise1')
resolve();
console.log('promise2')
}).then(function(){
console.log('promise3')
})
console.log('script end')
/* script start
async1 start
async2
promise1
promise2
script end
nextTick
async1 end
promise3
setTimeout0
setTimeout3
setImmediate
*/
4
setTimeout(() => {
console.log('timeout0');
new Promise((resolve, reject) => {
resolve('resolved') }).
then(res => console.log(res));
new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('timeout resolved')
})
}).then(res => console.log(res));
process.nextTick(() => {
console.log('nextTick1');
process.nextTick(() => {
console.log('nextTick2');
});
});
process.nextTick(() => {
console.log('nextTick3');
});
console.log('sync');
setTimeout(() => {
console.log('timeout2');
}, 0);
}, 0);
/*timeout0
sync
nextTick1
nextTick3
nextTick2
resolved
timeout resolved
timeout2
*/
5.process.nextTick()導致程式餓死
const fs = require('fs');
function addNextTickRecurs(count) {
let self = this;
if (self.id === undefined) {
self.id = 0;
}
if (self.id === count) return;
process.nextTick(() => {
console.log(`process.nextTick call ${++self.id}`);
addNextTickRecurs.call(self, count);
});
}
addNextTickRecurs(Infinity);
setTimeout(console.log.bind(console, 'omg! setTimeout was called'), 10);
setImmediate(console.log.bind(console, 'omg! setImmediate also was called'));
fs.readFile(__filename, () => {
console.log('omg! file read complete callback was called!');
});
console.log('started');
/*
process.nextTick call 1
process.nextTick call 2
process.nextTick call 3
process.nextTick call 4
process.nextTick call 5
...
*/
- process.nextTick()的nextTickQueue在每個階段執行完都會檢查執行一次,並且在nextTick裡增加的nextTick會直接新增到nextTickQueue佇列裡
setImmediate(() => console.log('this is set immediate 1'));
setImmediate(() => console.log('this is set immediate 2'));
setImmediate(() => console.log('this is set immediate 3'));
setTimeout(() => console.log('this is set timeout 1'), 0);
setTimeout(() => {
console.log('this is set timeout 2');
process.nextTick(() => console.log('this is process.nextTick added inside setTimeout'));
}, 0);
setTimeout(() => console.log('this is set timeout 3'), 0);
setTimeout(() => console.log('this is set timeout 4'), 0);
setTimeout(() => console.log('this is set timeout 5'), 0);
process.nextTick(() => console.log('this is process.nextTick 1'));
process.nextTick(() => {
process.nextTick(console.log.bind(console, 'this is the inner next tick inside next tick'));
});
process.nextTick(() => console.log('this is process.nextTick 2'));
process.nextTick(() => console.log('this is process.nextTick 3'));
process.nextTick(() => console.log('this is process.nextTick 4'));
/*
this is process.nextTick 1
this is process.nextTick 2
this is process.nextTick 3
this is process.nextTick 4
this is the inner next tick inside next tick
this is set timeout 1
this is set timeout 2
this is process.nextTick added inside setTimeout
this is set timeout 3
this is set timeout 4
this is set timeout 5
this is set immediate 1
this is set immediate 2
this is set immediate 3
*/
7.process.nextTick和Promise的回撥函式
Promise.resolve().then(() => console.log('promise1 resolved'));
Promise.resolve().then(() => console.log('promise2 resolved'));
Promise.resolve().then(() => {
console.log('promise3 resolved');
process.nextTick(() => console.log('next tick inside promise resolve handler'));
});
Promise.resolve().then(() => console.log('promise4 resolved'));
Promise.resolve().then(() => console.log('promise5 resolved'));
setImmediate(() => console.log('set immediate1'));
setImmediate(() => console.log('set immediate2'));
process.nextTick(() => console.log('next tick1'));
process.nextTick(() => console.log('next tick2'));
process.nextTick(() => console.log('next tick3'));
setTimeout(() => console.log('set timeout'), 0);
setImmediate(() => console.log('set immediate3'));
setImmediate(() => console.log('set immediate4'));
/*
next tick1
next tick2
next tick3
promise1 resolved
promise2 resolved
promise3 resolved
promise4 resolved
promise5 resolved
next tick inside promise resolve handler
set timeout
set immediate1
set immediate2
set immediate3
set immediate4
*/