衆所周知,JavaScript 是單線程的,而 Nodejs 又能夠實現無阻塞的 I/O 操做,就是由於 Event Loop 的存在。javascript
Event Loop 主要有如下幾個階段,一個矩形表明着一個階段,以下所示:java
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
複製代碼
每一個階段都會維護一個先進先出的隊列結構,當事件循環進入某個階段時,就會執行該階段的一些特定操做,而後依序執行隊列中的回調函數。 當隊列中的回調函數都執行完畢或者已執行的回調函數的個數達到某個最大值,就會進入下一個階段。node
事件循環的開始階段,執行 setTimeout
和 setInterval
的回調函數。git
當定時器規定的時間到了以後,就會將定時器的回調函數放入隊列中,而後依序執行。github
假如你有 a、b、c 四個定時器,時間間隔分別爲 10ms 、20ms 、 30ms。當進入事件循環的 timer
階段時,時間過去了 25 ms,那麼定時器 a 和 b 的回調就會被執行,執行完畢後就進入下一個階段。canvas
執行除了 setTimeout
、 setInterval
、 setImmediate
和 close callbacks
等的回調函數。promise
進行一些內部操做。bash
這應該是事件循環中最重要的一個階段了。異步
若是這個階段的隊列不爲空,那麼隊列中的回調會被順序執行;若是隊列爲空,也有 setImmediate
函數被調用,那麼就會進入 check
階段。若是隊列爲空且沒有 setImmediate
的函數調用,事件循環會進行等待,一旦有回調函數被添加到隊列中時,當即執行。socket
setImmediate
的回調會在這個階段被執行。
例如 socket.on('close', ...)
等的回調在這個階段執行。
// timeout_vs_immediate_1.js
setTimeout(function timeout() {
console.log('timeout');
}, 0);
setImmediate(function immediate() {
console.log('immediate');
});
複製代碼
按照以前的說法,事件循環會先進入 timer
階段,執行 setTimeout
的回調,等到進入 check
階段時, setImmediate
的回調纔會被執行。因此有一些人認爲上面的代碼的輸出結果應該是:
$ node timeout_vs_immediate_1.js
timeout
immediate
複製代碼
但其實這裏的結果是不肯定的。這裏每每跟進程的性能有關係,並且,這裏 setTimeout
的間隔雖然是 0,實際上會是 1。因此當啓動程序進入事件循環,時間還未過去 1ms 時,timer 階段的隊列是空的,不會有回調被執行。而這裏又有 setImmediate
函數的調用,因此以後走到 check
階段時,setImmediate
的回調會被調用。若是事件循環進入 timer
階段時,已經消耗了 1ms ,那麼這個時候 setTimeout
的回調就會被執行,以前進入到 check
階段,再執行 setImmediate
的回調。
因此,如下的兩種輸出均可能出現。
$ node timeout_vs_immediate_1.js
timeout
immediate
$ node timeout_vs_immediate_1.js
immediate
timeout
複製代碼
假如,上面的代碼是放在一個 I/0 循環內,如
// timeout_vs_immediate_2.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
複製代碼
那麼結果就是肯定的,輸出結果以下
$ node timeout_vs_immediate_2.js
immediate
timeout
複製代碼
process.nextTick()
並不屬於事件循環中的一部分,但也是異步的 API 。
當前的操做完成了以後,若是有 process.nextTick()
的調用,那麼 process.nextTick()
中的回調會接着被執行,若是回調裏面又有 process.nextTick()
,那麼回調中的 process.nextTick()
的回調也會接着被執行。因此 procee.nextTick()
可能會阻塞事件循環進入下一個階段。
// process_nexttick_1.js
let i = 0;
function foo() {
i += 1;
if (i > 3) return;
console.log('foo func');
setTimeout(() => {
console.log('timeout');
}, 0);
process.nextTick(foo);
}
setTimeout(foo, 5);
複製代碼
按照以前的說法,上面的輸出結果以下:
$ node process_nexttick_1.js
foo func
foo func
foo func
timeout
timeout
timeout
複製代碼
你可能會有一個疑問,process.nextTick()
是在事件循環某個階段的隊列都清空以後再執行,仍是在隊列中某個回調執行完成後接着執行。想一想下面的代碼的輸出結果是什麼?
// process_nexttick_2.js
let i = 0;
function foo() {
i += 1;
if (i > 2) return;
console.log('foo func');
setTimeout(() => {
console.log('timeout');
}, 0);
process.nextTick(foo);
}
setTimeout(foo, 2);
setTimeout(() => {
console.log('another timeout');
}, 2);
複製代碼
執行一下,看看結果
// node version: v11.12.0
$ node process_nexttick_2.js
foo func
foo func
another timeout
timeout
timeout
複製代碼
結果如上所示,process.nextTick()
是在隊列中的某個回調完成後就接着執行的。
你看到上面的結果,有個註釋 node version: v11.12.0
。 即運行這段代碼的 node 版本爲 11.12.0 ,若是你的 node 版本是低於這個的,如 7.10.1 ,可能就會獲得不一樣的結果。
// node version: v7.10.0
$ node process_nexttick_2.js
foo func
another timeout
foo func
timeout
timeout
複製代碼
不一樣的版本表現不一樣,我想應該是新的版本作了更新調整。
process.nextTick()
對應的是 nextTickQueue,Promise
對應的是 microTaskQueue 。
這二者都不屬於事件循環的某個部分,但它們執行的時機都是在當前的某個操做以後,那這二者的執行前後呢
// process_nexttick_vs_promise.js
let i = 0;
function foo() {
i += 1;
if (i > 2) return;
console.log('foo func');
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(foo);
}
setTimeout(foo, 0);
Promise.resolve().then(() => {
console.log('promise');
});
process.nextTick(() => {
console.log('nexttick');
});
複製代碼
運行代碼,結果以下:
$ node process_nexttick_vs_promise.js
nexttick
promise
foo func
foo func
timeout
timeout
複製代碼
若是你都搞懂了上面的輸出結果是爲什麼,那麼對於 Nodejs 中的事件循環你也就能夠掌握了。
歡迎你們一塊兒討論,有不錯的地方請指正。