第一次翻譯文檔,渣翻請見諒。
原文鏈接 https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
參考鏈接 http://www.cnblogs.com/MuYunyun/p/7287413.html
http://www.cnblogs.com/MuYunyun/p/7287413.html
什么是事件循環(huán)治唤?
事件循環(huán)是使Node.js得以執(zhí)行非阻塞I/O操作的——即使JavaScript是單線程的泽裳,通過在任何可能的時候?qū)⒉僮鱫ffload到系統(tǒng)內(nèi)核笋额。
最現(xiàn)代的內(nèi)核是多線程的冯袍,它們能處理多個操作在后臺執(zhí)行犬钢。當這些操作的其中之一完成苍鲜,內(nèi)核告訴Node.js,使得合適的callback可以被加入到poll隊列中玷犹,最終被執(zhí)行混滔。
事件循環(huán)解釋
當Node.js啟動,它初始化事件循環(huán)歹颓,處理提供的輸入腳本(或丟入REPL)坯屿,這可能導致異步API調(diào)用,調(diào)度定時器巍扛,或者調(diào)用process.nextTick()愿伴,然后開始處理事件循環(huán)。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections,┤
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
每個階段有一個callback的FIFO隊列等待執(zhí)行电湘。一般來說,當事件循環(huán)進入到一個給定的階段鹅经,它會執(zhí)行這個階段的所有具體的操作寂呛,然后執(zhí)行這個階段的隊列中的callback,直到隊列好近或callback 的最大數(shù)量被執(zhí)行瘾晃。之后事件循環(huán)會移動到下一個階段贷痪。
階段概覽
- timers: 計時器,這個階段執(zhí)行通過setTimeout()和setInterval()注冊的回調(diào)函數(shù)蹦误。
- pending callbacks: 執(zhí)行被推遲到下個循環(huán)迭代的I/O回調(diào)函數(shù)劫拢,大部分回調(diào)將在這里被處理
- idle, prepare: 只在內(nèi)部使用
- poll: 輪詢,對接著要處理的I/O事件進行新的輪詢强胰,執(zhí)行與I/O相關的回調(diào)函數(shù)(幾乎所有舱沧,除了close callbacks,這個通過即時起注冊偶洋,以及setImmediate())
- check: setImmediate()回調(diào)函數(shù)在這里被調(diào)用
-
close callbacks: 一些關閉回調(diào)函數(shù)熟吏,如socket.on('close', ...), 處理所有‘結(jié)束’事件的回調(diào)。
在每輪事件循環(huán)期間玄窝,Node.js檢查是否在等待任何異步I/O或計時器牵寺,如果沒有,就完整關閉恩脂。
階段細節(jié)
timers
一個定時器指定閾值帽氓,一個提供的回調(diào)函數(shù)可能在這個threshold閾值之后,而不是我們想要它被執(zhí)行的exact實際的時間被執(zhí)行俩块。定時器的回調(diào)函數(shù)會在它們被指派在指定長度的時間之后盡早執(zhí)行黎休;然而浓领,操作系統(tǒng)調(diào)度或其他回調(diào)函數(shù)的運行可能會推遲它們。
注意:理論上奋渔,exact輪詢階段控制定時器何時被執(zhí)行镊逝。
例如,你設置一個timeout在100ms的閾值吼執(zhí)行嫉鲸,然后你的攪拌開始異步讀取一個文件撑蒜,讀取文件花費95ms:
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
}
});
當事件循環(huán)進入poll輪詢階段,它有一個空的隊列(fs.readFile()
尚未完成)玄渗,因此它會等待剩余的毫秒數(shù)直到達到最快定時器的閾值座菠。當?shù)却?5ms,fs.readFile()
完成讀取文件藤树,它的花費10ms來完成的回調(diào)函數(shù)被加入到poll隊列并被執(zhí)行浴滴。當回調(diào)函數(shù)完成,隊列中沒有其他的回調(diào)函數(shù)了岁钓,于是事件循環(huán)會看最快定時器的閾值已經(jīng)被達到升略,然后wrap back回到timers定時器階段來執(zhí)行定時器的回調(diào)函數(shù)。在這個例子中屡限,你會看到定時器被設置和它的回調(diào)函數(shù)被執(zhí)行的總延遲為105ms品嚣。
注意: 為了避免poll輪詢階段耗盡事件循環(huán),libuv(實現(xiàn)Node.js事件循環(huán)和平臺的所有異步行為的C庫)也有一個hard最大值(系統(tǒng)依賴)來阻止對更多事件輪詢钧大。
pending callbacks
這個階段為一些系統(tǒng)操作翰撑,如TCP錯誤類型,執(zhí)行回調(diào)函數(shù)啊央。例如眶诈,如果一個TCP套接字在嘗試連接時接收到了ECONNREFUSED
,一些系統(tǒng)想等待來報告這個錯誤瓜饥。這回被放入pending callbacks的隊列中來執(zhí)行逝撬。
poll
poll輪詢階段有兩個主要的功能:
- 計算它該阻塞多久和輪詢I/O, 然后
- 處理poll隊列中的事件乓土。
當事件循環(huán)進入poll階段球拦,且沒有定時器被設置,兩件事之一會發(fā)生:
- 如果poll階段的隊列不為空帐我,則事件循環(huán)會遍歷回調(diào)函數(shù)的隊列坎炼,同步執(zhí)行它們,直到隊列為空或達到系統(tǒng)依賴的hard limit拦键。
- 如果poll階段的隊列為空谣光,則以下兩件事之一會發(fā)生:
- 如果腳本已經(jīng)被
setImmediate()
設置,事件循環(huán)會終止poll階段并繼續(xù)到check階段來執(zhí)行那些被設置的腳本芬为。 - 如果腳本尚未被
setImmediate()
設置萄金,事件循環(huán)會等待回調(diào)函數(shù)被加入到隊列中蟀悦,然后立刻執(zhí)行它們
- 如果腳本已經(jīng)被
一旦poll隊列為空,事件循環(huán)會檢查timers定時器氧敢,它們的時間閾值被達到日戈。如果一個或多個定時器就緒,事件循環(huán)會回到timers階段來執(zhí)行那些定時器的回調(diào)函數(shù)孙乖。
check
這個階段允許我們在poll階段完成后立即執(zhí)行回調(diào)函數(shù)浙炼。如果poll階段變?yōu)殚e置,且腳本被setImmediate()
設置唯袄,事件循環(huán)會來到check階段而不是等待弯屈。
setImmediate()
實際是一個特殊的定時器,運行在事件循環(huán)的分開的階段恋拷。它使用一個libuvAPI资厉,這個API設置回調(diào)函數(shù)在poll階段完成后來執(zhí)行。
一般來說蔬顾,在代碼被執(zhí)行時宴偿,事件循環(huán)最終會進入poll階段,在這里它會等待一個到來的連接诀豁,請求窄刘,等等。然而且叁,如果一個回調(diào)函數(shù)已經(jīng)被setImmediate()
設置且poll階段變?yōu)殚e置,它會終止并進入到check階段而不是等待poll輪詢事件秩伞。
close callbacks
如果一個socket或handle被突然關閉(e.g. socket.destroy()
)逞带,這個'close'
事件會在這個階段被射出。否則它會通過process.nextTick()
被射出纱新。
setImmediate()
VS setTimeout()
兩者類似展氓,但是根據(jù)被調(diào)用的時間會發(fā)生不同的行為。
-
setImmediate()
被設計為一旦現(xiàn)在的poll階段完成就執(zhí)行脸爱。 -
setTimeout()
設置一個腳本在一個最小毫米單位閾值過去之后執(zhí)行遇汞。
定時器被執(zhí)行的順序會根據(jù)它們被調(diào)用時處在的上下文而變化。如果都在主模塊中被調(diào)用簿废,則timing會被進程的性能限制(進程的性能可能會被機器上運行的其他應用程序影響)空入。
例如,如果我們運行以下的不處于一個I/O cycle(也就是主模塊)內(nèi)部的腳本族檬,這兩個定時器被執(zhí)行的順序是不一定的歪赢,因為受到了進程性能的限制:
// 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
然而,如果你把兩個調(diào)用移動到一個I/O cycle里面单料,immediate 回調(diào)函數(shù)總是先執(zhí)行:
// 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)點是埋凯,當處在I/O cycle之內(nèi)時点楼,前者總是在任何其他定時器之前執(zhí)行,不管當前有多少個定時器白对。