本文為譯文账忘,英文原文
什么是事件循環(huán)
盡管JavaScript是單線程的鳖擒,但通過盡可能將操作放到系統(tǒng)內(nèi)核執(zhí)行蒋荚,事件循環(huán)允許Node.js執(zhí)行非阻塞I/O操作期升。
由于現(xiàn)代大多數(shù)內(nèi)核都是多線程的播赁,因此它們可以處理在后臺執(zhí)行的多個操作行拢。 當(dāng)其中一個操作完成時诞吱,內(nèi)核會告訴Node.js沼瘫,以便可以將相應(yīng)的回調(diào)添加到 輪詢隊列 中以最終執(zhí)行耿戚。 我們將在本主題后面進(jìn)一步詳細(xì)解釋。
事件循環(huán)解釋
當(dāng)Node.js啟動時坛猪,它初始化事件循環(huán),處理提供的輸入腳本(或放入REPL就斤,本文檔未涉及)洋机,這可能會進(jìn)行異步API調(diào)用绷旗,調(diào)度計時器或調(diào)用process.nextTick()
, 然后開始處理事件循環(huán)址晕。
下圖顯示了事件循環(huán)操作順序的簡要概述启搂。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
注意:每個框都將被稱為事件循環(huán)的“階段”胳赌。
每個階段都要執(zhí)行一個FIFO的回調(diào)隊列。 雖然每個階段都有其特殊的方式捍掺,但通常挺勿,當(dāng)事件循環(huán)進(jìn)入給定階段時不瓶,它將執(zhí)行特定于該階段的任何操作禾嫉,然后在該階段的隊列中執(zhí)行回調(diào),直到隊列耗盡或最大回調(diào)數(shù)量為止 蚊丐。 當(dāng)隊列耗盡或達(dá)到回調(diào)限制時熙参,事件循環(huán)將移至下一階段,依此類推麦备。
由于這些操作中的任何一個可以調(diào)度更多操作并且在輪詢階段中處理的新事件由內(nèi)核排隊尊惰,因此輪詢事件可以在處理輪詢事件時排隊。 因此泥兰,長時間運行的回調(diào)可以允許輪詢階段運行的時間比計時器的閾值長得多。 有關(guān)詳細(xì)信息题禀,請參閱計時器和輪詢部分鞋诗。
注意:Windows和Unix / Linux實現(xiàn)之間存在輕微差異削彬,但這對于此演示并不重要神僵。 最重要的部分在這里。 實際上有七到八個步驟,但我們關(guān)心的是 - Node.js實際使用的那些 - 是上面那些。
階段概述
-
timer : 此階段執(zhí)行
setTimeout()
和setInterval()
調(diào)度的回調(diào) - pending callbacks : 執(zhí)行延遲到下一個循環(huán)迭代的I/O回調(diào)
- idle, prepare : 只用于內(nèi)部
-
poll : 檢索新的I/O事件; 執(zhí)行與I/O相關(guān)的回調(diào)(幾乎所有回調(diào)都是帶有異常的
close callbacks
,timers
和setImmediate()
調(diào)度的回調(diào)); node將在適當(dāng)?shù)臅r候阻塞在這里 -
check : 這里調(diào)用
setImmediate()
回調(diào)函數(shù) - close callbacks : 一些 close callbacks, 例如. socket.on('close', ...)
在事件循環(huán)的每次運行之間它呀,Node.js檢查它是否在等待任何異步I / O或定時器奢人,如果沒有土辩,則關(guān)閉。
階段細(xì)節(jié)
定時器(timer)
計時器在一個回調(diào)執(zhí)行完之后指定閾值,而不是人們希望的確切時間去執(zhí)行。 定時器回調(diào)將在指定的時間過去后盡早安排; 但是蒸殿,操作系統(tǒng)調(diào)度或其他回調(diào)的運行可能會延遲它們楣铁。
注意:從技術(shù)上講浓镜,輪詢階段控制何時執(zhí)行定時器。
例如,假設(shè)您計劃在100毫秒后執(zhí)行timeout
硼婿,然后您的腳本將異步讀取一個耗時95毫秒的文件:
const fs = require('fs');
function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
}, 100);
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});
當(dāng)事件循環(huán)進(jìn)入輪詢階段時逸月,它有一個空隊列(fs.readFile()
尚未完成)肛响,因此它將等待剩余的ms數(shù)猎物,直到達(dá)到最快的計時器閾值。 當(dāng)它等待95毫秒傳遞時搀罢,fs.readFile()
完成讀取文件铅鲤,并且其完成需要10毫秒的回調(diào)被添加到輪詢隊列并執(zhí)行。 當(dāng)回調(diào)結(jié)束時,隊列中不再有回調(diào),因此事件循環(huán)將看到已達(dá)到最快定時器的閾值溢陪,然后回繞到定時器階段以執(zhí)行定時器的回調(diào)邓馒。 在此示例中挂疆,您將看到正在調(diào)度的計時器與正在執(zhí)行的回調(diào)之間的總延遲將為105毫秒。
注意:為了防止輪詢階段使事件循環(huán)挨餓锈拨,libuv(實現(xiàn)Node.js事件循環(huán)的C庫和平臺的所有異步行為)在停止輪詢之前也為事件提供了固定的最大值(取決于系統(tǒng))谷浅。
等待回調(diào)(pending callbacks)
此階段執(zhí)行某些系統(tǒng)操作(例如TCP錯誤類型)的回調(diào)。 例如兔院,如果TCP套接字在嘗試連接時收到 ECONNREFUSED
,則某些*nix系統(tǒng)希望等待報告錯誤蛛勉。 這將排隊等待在等待回調(diào)階段執(zhí)行。
輪詢(poll)
輪詢階段有兩個主要功能:
- 計算它阻塞和輪詢I / O的時間妇拯,然后
- 處理輪詢隊列中的事件丹弱。
當(dāng)事件循環(huán)進(jìn)入輪詢階段并且沒有定時器調(diào)度時粹湃,將發(fā)生以下兩種情況之一:
如果輪詢隊列不為空构捡,則事件循環(huán)將遍歷回調(diào)隊列并且同步執(zhí)行吹由,直到隊列已執(zhí)行完或者達(dá)到系統(tǒng)相關(guān)的固定限制磕道。
-
如果輪詢隊列為空,則會發(fā)生以下兩種情況之一:
如果
setImmediate()
已調(diào)度腳本纵搁,則事件循環(huán)將結(jié)束輪詢階段并繼續(xù)執(zhí)行檢查階段以執(zhí)行這些調(diào)度腳本跷敬。如果
setImmediate()
尚未調(diào)度腳本饺鹃,則事件循環(huán)將等待將回調(diào)添加到隊列,然后立即執(zhí)行它們。
檢查(check)
此階段允許在輪詢階段完成后立即執(zhí)行回調(diào)肴敛。 如果輪詢階段變?yōu)榭臻e并且腳本已使用setImmediate()
排隊医男,則事件循環(huán)可以繼續(xù)到檢查階段而不是等待研底。
setImmediate()
實際上是一個特殊的計時器,它在事件循環(huán)的一個單獨階段運行吨掌。 它使用libuv API來調(diào)度在輪詢階段完成后執(zhí)行的回調(diào)。
通常枢贿,在執(zhí)行代碼時殉农,事件循環(huán)最終會到達(dá)輪詢階段,它將等待傳入連接局荚,請求等超凳。但是,如果已使用setImmediate()
調(diào)度回調(diào)并且輪詢階段變?yōu)榭臻e耀态,則 將結(jié)束并繼續(xù)檢查階段轮傍,而不是等待輪詢事件。
關(guān)閉回調(diào)(close callbacks)
如果套接字或句柄突然關(guān)閉(例如socket.destroy()
)茫陆,則在此階段將發(fā)出'close'
事件金麸。 否則它將通過process.nextTick()
發(fā)出擎析。
setImmediate()
vs setTimeout()
setImmediate
和setTimeout()
類似簿盅,但根據(jù)它們的調(diào)用時間以不同的方式運行挥下。
-
setImmediate()
用于在當(dāng)前輪詢階段完成后執(zhí)行腳本。 -
setTimeout()
計劃在經(jīng)過最小閾值(以ms為單位)后運行的腳本桨醋。
執(zhí)行定時器的順序?qū)⒏鶕?jù)調(diào)用它們的上下文而有所不同棚瘟。 如果從主模塊中調(diào)用兩者,則時間將受到進(jìn)程性能的限制(可能受到計算機(jī)上運行的其他應(yīng)用程序的影響)喜最。
例如偎蘸,如果我們運行不在I / O周期內(nèi)的以下腳本(即主模塊),則執(zhí)行兩個定時器的順序是不確定的瞬内,因為它受進(jìn)程性能的約束:
// timeout_vs_immediate.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout
但是迷雪,如果在I / O周期內(nèi)移動兩個調(diào)用,則始終首先執(zhí)行立即回調(diào):
// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
$ node timeout_vs_immediate.js
immediate
timeout
$ node timeout_vs_immediate.js
immediate
timeout
使用setImmediate()
而不是setTimeout()
的主要優(yōu)點是setImmediate()
將始終在任何定時器之前執(zhí)行(如果在I / O周期內(nèi)調(diào)度)虫蝶,與存在多少定時器無關(guān)章咧。
process.nextTick()
理解process.nextTick()
您可能已經(jīng)注意到process.nextTick()
沒有顯示在圖中,即使它是異步API的一部分能真。 這是因為process.nextTick()
在技術(shù)上不是事件循環(huán)的一部分赁严。 相反,nextTickQueue
將在當(dāng)前操作完成后處理粉铐,而不管事件循環(huán)的當(dāng)前階段如何疼约。
回顧一下我們的圖表,無論何時在給定階段調(diào)用process.nextTick()
蝙泼,傳遞給process.nextTick()
的所有回調(diào)都將在事件循環(huán)繼續(xù)之前得到解決程剥。 這可能會產(chǎn)生一些不好的情況,因為它允許您通過進(jìn)行遞歸的process.nextTick()
調(diào)用來“餓死”您的I / O踱承,這會阻止事件循環(huán)到達(dá)輪詢階段倡缠。
為什么會被允許?
為什么這樣的東西會被包含在Node.js中茎活? 其中一部分是一種設(shè)計理念昙沦,其中API應(yīng)該始終是異步的,即使它不是必須的载荔。 以此代碼段為例:
function apiCall(arg, callback) {
if (typeof arg !== 'string')
return process.nextTick(callback,
new TypeError('argument should be string'));
}
這段代碼進(jìn)行參數(shù)檢查盾饮,如果不正確,它會將錯誤傳遞給回調(diào)懒熙。 最近更新的API允許將參數(shù)傳遞給process.nextTick()
丘损,允許它將回調(diào)后傳遞的任何參數(shù)作為參數(shù)傳播到回調(diào),因此您不必嵌套函數(shù)工扎。
我們正在做的是將錯誤傳回給用戶徘钥,但只有在我們允許其余的用戶代碼執(zhí)行之后。 通過使用process.nextTick()
肢娘,我們保證apiCall()
始終在用戶代碼的其余部分之后和允許事件循環(huán)繼續(xù)之前運行其回調(diào)呈础。 為了實現(xiàn)這一點舆驶,允許JS調(diào)用堆棧展開然后立即執(zhí)行提供的回調(diào),這允許一個人對process.nextTick()
進(jìn)行遞歸調(diào)用而不會達(dá)到RangeError
:超出v8的最大調(diào)用堆棧大小而钞。
這種理念可能會導(dǎo)致一些潛在的問題沙廉。 以此片段為例:
let bar;
// this has an asynchronous signature, but calls callback synchronously
function someAsyncApiCall(callback) { callback(); }
// the callback is called before `someAsyncApiCall` completes.
someAsyncApiCall(() => {
// since someAsyncApiCall has completed, bar hasn't been assigned any value
console.log('bar', bar); // undefined
});
bar = 1;
用戶將someAsyncApiCall()
定義為具有異步簽名,但它實際上是同步操作的臼节。 調(diào)用它時撬陵,在事件循環(huán)的同一階段調(diào)用提供給someAsyncApiCall()
的回調(diào),因為someAsyncApiCall()
實際上不會異步執(zhí)行任何操作网缝。 因此巨税,回調(diào)嘗試引用bar,即使它在范圍內(nèi)可能沒有該變量粉臊,因為該腳本無法運行完成垢夹。
通過將回調(diào)放在process.nextTick()
中,腳本仍然能夠運行完成维费,允許在調(diào)用回調(diào)之前初始化所有變量果元,函數(shù)等。 它還具有不允許事件循環(huán)繼續(xù)的優(yōu)點犀盟。 在允許事件循環(huán)繼續(xù)之前而晒,向用戶警告錯誤可能是有用的。 以下是使用process.nextTick()
的前一個示例:
let bar;
function someAsyncApiCall(callback) {
process.nextTick(callback);
}
someAsyncApiCall(() => {
console.log('bar', bar); // 1
});
bar = 1;
這是另一個真實世界的例子:
const server = net.createServer(() => {}).listen(8080);
server.on('listening', () => {});
僅傳遞端口時阅畴,端口立即綁定倡怎。 因此,可以立即調(diào)用'listening'
回調(diào)贱枣。 問題是那時候不會設(shè)置.on('listening')
回調(diào)监署。
為了解決這個問題,'listening'
事件在nextTick()
中排隊纽哥,以允許腳本運行完成钠乏。 這允許用戶設(shè)置他們想要的任何事件處理程序。
process.nextTick()
vs setImmediate()
就用戶而言春塌,我們有兩個類似的調(diào)用晓避,但它們的名稱令人困惑。
-
process.nextTick()
在同一階段立即觸發(fā) -
setImmediate()
在事件循環(huán)的后續(xù)迭代或'tick'中觸發(fā)
實質(zhì)上只壳,應(yīng)該交換名稱俏拱。 process.nextTick()
比setImmediate()
更快地觸發(fā),但這是過去創(chuàng)造的吼句,不太可能改變锅必。 進(jìn)行此切換會破壞npm上的大部分包。 每天都會添加更多新模塊惕艳,這意味著我們每天都在等待搞隐,更多的潛在破損發(fā)生分蓖。 雖然它們令人困惑,但自身的叫法不會改變尔许。
我們建議開發(fā)人員在所有情況下都使用setImmediate()
,因為它更容易推理(并且它導(dǎo)致代碼與更廣泛的環(huán)境兼容终娃,如瀏覽器JS味廊。)
為什么要使用process.nextTick()
?
有兩個主要原因:
- 允許用戶處理錯誤棠耕,清除任何不需要的資源余佛,或者在事件循環(huán)繼續(xù)之前再次嘗試請求。
- 有時需要允許回調(diào)在調(diào)用堆棧展開之后但在事件循環(huán)繼續(xù)之前運行窍荧。
一個例子是匹配用戶的期望辉巡。 簡單的例子:
const server = net.createServer();
server.on('connection', (conn) => { });
server.listen(8080);
server.on('listening', () => { });
假設(shè)listen()
在事件循環(huán)開始時運行,但是監(jiān)聽回調(diào)放在setImmediate()
中蕊退。 除非傳遞主機(jī)名郊楣,否則將立即綁定到端口。 要使事件循環(huán)繼續(xù)瓤荔,它必須達(dá)到輪詢階段净蚤,這意味著可能已經(jīng)接收到連接的非零概率允許在偵聽事件之前觸發(fā)連接事件。
另一個例子是運行一個函數(shù)構(gòu)造函數(shù)输硝,比如繼承自EventEmitter
今瀑,它想在構(gòu)造函數(shù)中調(diào)用一個事件:
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
this.emit('event');
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
您無法立即從構(gòu)造函數(shù)中發(fā)出事件,因為腳本將不會處理到用戶為該事件分配回調(diào)的位置点把。 因此橘荠,在構(gòu)造函數(shù)本身中,您可以使用process.nextTick()
來設(shè)置回調(diào)以在構(gòu)造函數(shù)完成后發(fā)出事件郎逃,從而提供預(yù)期的結(jié)果:
const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
EventEmitter.call(this);
// use nextTick to emit the event once a handler is assigned
process.nextTick(() => {
this.emit('event');
});
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});