翻譯完了以後,才發現有官方翻譯;可是本文更加全面。本文是從官方文檔和多篇文章整合而來。javascript
看完本文以後,你會發現這裏內容與《NodeJs深刻淺出》第三章第四節3.4 非I/O異步API
中的內容不吻合。由於書上是有些內容是錯誤的。html
事件循環使Node.js
能夠經過將操做轉移到系統內核中來執行非阻塞I/O
操做(儘管JavaScript
是單線程的)。java
因爲大多數現代內核都是多線程的,所以它們能夠處理在後臺執行的多個操做。 當這些操做之一完成時,內核會告訴Node.js
,以即可以將適當的回調添加到輪詢隊列中以最終執行。 咱們將在本文的後面對此進行詳細說明。node
Node.js
啓動時,它將初始化事件循環,處理提供的輸入腳本(或放入REPL,本文檔未涵蓋),這些腳本可能會進行異步API調用,調度計時器或調用process.nextTick
, 而後開始處理事件循環。web
下圖顯示了事件循環操做順序的簡化概述。npm
┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘
每一個階段都有一個要執行的回調FIFO隊列。 儘管每一個階段都有其本身的特殊方式,可是一般,當事件循環進入給定階段時,它將執行該階段特定的任何操做,而後在該階段的隊列中執行回調,直到隊列耗盡或執行回調的最大數量爲止。 當隊列已爲空或達到回調限制時,事件循環將移至下一個階段,依此類推。
因爲這些操做中的任何一個均可能調度更多操做,而且在poll階段
處理由內核排隊的新事件(好比I/O
事件),所以能夠在處理poll
事件時將poll
事件排隊。 最終致使的結果是,長時間運行的回調可以使poll
階段運行的時間比timer
的閾值長得多。 有關更多詳細信息,請參見計時器(timer)和輪詢(poll)部分。api
注意:Windows和Unix / Linux實現之間存在細微差別,但這對於本演示並不重要。 最重要的部分在這裏。 實際上有七個或八個階段,可是咱們關心的那些(Node.js實際使用的那些)是上面的階段。promise
I/O
回調。I/O
事件;執行與I/O
相關的回調(除了關閉回調,計時器調度的回調和setImmediate
以外,幾乎全部這些回調) 適當時,node
將在此處阻塞。setImmediate
回調。socket.on('close', ...)
。在每次事件循環運行之間,Node.js會檢查它是否正在等待任何異步I/O
或timers
,若是沒有,則將其乾淨地關閉。瀏覽器
計時器能夠在回調後面指定時間閾值,但這不是咱們但願其執行的確切時間。 計時器回調將在通過指定的時間後儘早運行。 可是,操做系統調度或其餘回調的運行可能會延遲它們。-- 執行的實際時間不肯定
多線程
注意:從技術上講,輪詢(poll
)階段控制計時器的執行時間。
例如,假設你計劃在100毫秒後執行回調,而後腳本開始異步讀取耗時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 } });
當事件循環進入poll
階段時,它有一個空隊列(fs.readFile
還沒有完成),所以它將等待直到達到最快的計時器timer
閾值爲止。 等待95 ms
過去時,fs.readFile
完成讀取文件,並將須要10ms
完成的其回調添加到輪詢(poll
)隊列並執行。 回調完成後,隊列中再也不有回調,此時事件循環已達到最先計時器(timer
)的閾值(100ms
),而後返回到計時器(timer
)階段以執行計時器的回調。 在此示例中,您將看到計劃的計時器與執行的回調之間的總延遲爲105ms
。
Note: To prevent the poll phase from starving the event loop, libuv (the C library that implements the Node.js event loop and all of the asynchronous behaviors of the platform) also has a hard maximum (system dependent) before it stops polling for more events.
注意:爲防止輪詢poll
階段使事件循環陷入飢餓狀態(一直等待poll
事件),libuv還具備一個硬最大值限制來中止輪詢。
此階段執行某些系統操做的回調,例如TCP錯誤。 舉個例子,若是TCP套接字在嘗試鏈接時收到ECONNREFUSED,則某些* nix系統但願等待報告錯誤。 這將會在
pending callbacks
階段排隊執行。
輪詢階段具備兩個主要功能:
當事件循環進入輪詢(poll
)階段而且沒有任何計時器調度( timers scheduled)時,將發生如下兩種狀況之一:
究竟是哪些硬限制?
)。若是輪詢隊列爲空,則會發生如下兩種狀況之一:
setImmediate
調度了腳本,則事件循環將結束輪詢poll
階段,並繼續執行check
階段以執行那些調度的腳本。setImmediate
設置回調,則事件循環將等待poll
隊列中的回調,而後當即執行它們。一旦輪詢隊列(poll queue
)爲空,事件循環將檢查哪些計時器timer
已經到時間。 若是一個或多個計時器timer
準備就緒,則事件循環將返回到計時器階段,以執行這些計時器的回調。
此階段容許在輪詢poll
階段完成後當即執行回調。 若是輪詢poll
階段處於空閒,而且腳本已使用setImmediate
進入check
隊列,則事件循環可能會進入check
階段,而不是在poll
階段等待。
setImmediate
其實是一個特殊的計時器,它在事件循環的單獨階段運行。 它使用libuv API
,該API計劃在輪詢階段完成後執行回調。
一般,在執行代碼時,事件循環最終將到達輪詢poll
階段,在該階段它將等待傳入的鏈接,請求等。可是,若是已使用setImmediate
設置回調而且輪詢階段變爲空閒,則它將將結束並進入check
階段,而不是等待輪詢事件。
若是套接字或句柄忽然關閉(例如socket.destroy
),則在此階段將發出'close'事件。 不然它將經過process.nextTick
發出。
setImmediate
和setTimeout
類似,可是根據調用時間的不一樣,它們的行爲也不一樣。
setImmediate
設計爲在當前輪詢poll
階段完成後執行腳本。setTimeout
計劃在以毫秒爲單位的最小閾值過去以後運行腳本。計時器的執行順序將根據調用它們的上下文而有所不一樣。 若是二者都是主模塊(main module)中調用的,則時序將受到進程性能的限制(這可能會受到計算機上運行的其餘應用程序的影響)。有點難懂,舉個例子:
例如,若是咱們運行如下不在I/O
回調(即主模塊)內的腳本,則兩個計時器的執行順序是不肯定
的,由於它受進程性能的約束:
// 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
回調中,那麼immediate
老是執行第一:
// timeout_vs_immediate.js const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });
與setTimeout
相比,使用setImmediate
的主要優勢是,若是在I/O
週期內setImmediate
老是比任何timers
快。這個能夠在下方彩色圖中找到答案:poll
階段用setImmediate
設置下階段check
的回調,等到了check
就開始執行;timers
階段只能等到下次循環執行!
問題:那爲何在外部(好比主代碼部分 mainline
)這二者的執行順序不肯定呢?
解答:在mainline
部分執行setTimeout
設置定時器(沒有寫入隊列呦),與setImmediate
寫入check
隊列。mainline
執行完開始事件循環,第一階段是timers
,這時候timers
隊列可能爲空,也可能有回調;若是沒有那麼執行check
隊列的回調,下一輪循環在檢查並執行timers
隊列的回調;若是有就先執行timers
的回調,再執行check
階段的回調。所以這是timers
的不肯定性致使的。
觸類旁通:timers
階段寫入check
隊列
setTimeout(() => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });
老是會輸出:
immediate timeout
const ITERATIONS_MAX = 2; let iteration = 0; const timeout = setInterval(() => { console.log('TIME PHASE START:' + iteration); if (iteration >= ITERATIONS_MAX) { clearInterval(timeout); console.log('TIME PHASE exceeded!'); } console.log('TIME PHASE END:' + iteration); ++iteration; }, 0); setTimeout(() => { console.log('TIME PHASE0'); setTimeout(() => { console.log('TIME PHASE1'); setTimeout(() => { console.log('TIME PHASE2'); }); }); });
輸出:
TIME PHASE START:0 TIME PHASE END:0 TIME PHASE0 TIME PHASE START:1 TIME PHASE END:1 TIME PHASE1 TIME PHASE START:2 TIME PHASE exceeded! TIME PHASE END:2 TIME PHASE2
這代表,能夠理解setInterval
是setTimeout
的嵌套調用的語法糖。setInterval(() => {}, 0)
是在每一次事件循環中添加回調到timers
隊列。所以不會阻止事件循環的繼續運行,在瀏覽器上也不會感到卡頓。
你可能已經注意到process.nextTick
並未顯示在圖中,即便它是異步API的一部分也是如此。 這是由於process.nextTick
從技術上講不是事件循環的一部分。 相反,不管事件循環的當前階段如何,都將在當前操做完成以後處理nextTickQueue
。 在此,將操做定義爲在C/C ++處理程序基礎下過渡並處理須要執行的JavaScript。
回顧一下咱們的圖,在給定階段裏能夠在任意時間調用process.nextTick
,傳遞給process.nextTick
的全部回調都將在事件循環繼續以前獲得解決。 這可能會致使一些不良狀況,由於它容許您經過進行遞歸process.nextTick
調用來讓I/O
處於"飢餓"
狀態,從而防止事件循環進入輪詢poll
階段。
注意:Microtask callbacks 微服務
爲何這樣的東西會包含在Node.js中? 它的一部分是一種設計理念,即便不是必須的狀況下,API也應始終是異步的。
舉個例子:
function apiCall(arg, callback) { if (typeof arg !== 'string') return process.nextTick(callback, new TypeError('argument should be string')); } apiCall(1, e => console.log(e)); console.log(2); // 2 // 1
該代碼段會進行參數檢查,若是不正確,則會將錯誤傳遞給回調。 該API最近進行了更新,以容許將參數傳遞給process.nextTick
,從而能夠將回調後傳遞的全部參數都傳播爲回調的參數,所以您沒必要嵌套函數。
咱們正在作的是將錯誤傳遞迴用戶,但只有在咱們容許其他用戶的代碼執行以後。 經過使用process.nextTick
,咱們保證apiCall
始終在用戶的其他代碼以後以及事件循環繼續下階段以前運行其回調。 爲此,容許JS調用堆棧展開,而後當即執行所提供的回調,該回調能夠對process.nextTick
進行遞歸調用,而不會達到RangeError
:v8超出最大調用堆棧大小。
這種理念可能會致使某些潛在的問題狀況。 如下代碼段爲例:
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
定義爲具備異步簽名,但實際上它是同步運行的。 調用它時,提供給someAsyncApiCall
的回調在事件循環的同一階段被調用,由於someAsyncApiCall
實際上並不異步執行任何操做。 結果,即便腳本可能還沒有在範圍內,該回調也會嘗試引用bar
,由於該腳本沒法運行完畢。
經過將回調放置在process.nextTick
中,腳本仍具備運行完成的能力,容許在調用回調以前初始化全部變量,函數等。 它還具備不容許事件循環繼續下個階段的優勢。 在容許事件循環繼續以前,向用戶發出錯誤提示可能頗有用。 這是使用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', () => {});
僅經過端口時,該端口將當即綁定。 所以,能夠當即調用「監聽」回調。 問題在於那時還沒有設置.on('listening')
回調。
爲了解決這個問題,"listening"
事件在nextTick()
中排隊,以容許腳本運行完成。 這容許用戶設置他們想要的任何事件處理程序。
他們的調用方式很類似,可是名稱讓人困惑。
process.nextTick
在同一階段當即觸發setImmediate
fires on the following iteration or 'tick' of the event loop(在事件循環接下來的階段迭代中執行 - check階段)。本質上,名稱應互換。 process.nextTick
比setImmediate
觸發得更快,但因爲歷史緣由,不太可能改變。 進行此切換將破壞npm
上很大一部分軟件包。 天天都會添加更多的新模塊,這意味着咱們天天都在等待,更多潛在的損壞發生。 儘管它們使人困惑,但名稱自己不會改變。
咱們建議開發人員在全部狀況下都使用setImmediate
,由於這樣更容易推理(而且代碼與各類環境兼容,例如瀏覽器JS。)- 可是若是理解底層原理,就不同。
這裏舉出兩個緣由:
簡單的例子:
const server = net.createServer(); server.on('connection', (conn) => { }); server.listen(8080); server.on('listening', () => { }); // 設置監聽回調
假設listen
在事件循環的開始處運行,可是偵聽回調被放置在setImmediate
中(實際上listen
使用process.nextTick
,.on
在本階段完成)。 除非傳遞主機名,不然將當即綁定到端口。 爲了使事件循環繼續進行,它必須進入輪詢poll
階段,這意味着存在已經接收到鏈接可能性,從而致使在偵聽事件以前觸發鏈接事件(漏掉一些poll事件)。
另外一個示例正在運行一個要從EventEmitter
繼承的函數構造函數,它想在構造函數中調用一個事件:
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!'); });
你沒法當即從構造函數中發出事件,由於腳本還沒運行到開發者爲該事件分配回調的那裏(指myEmitter.on
)。 所以,在構造函數自己內,你可使用process.nextTick
設置構造函數完成後發出事件的回調,從而提供預期的結果:
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!'); });
來子一位外國小哥之手。連接在本文下面。
┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ nextTickQueue │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ nextTickQueue │ ┌─────────────┴─────────────┐ | | idle, prepare │ | └─────────────┬─────────────┘ nextTickQueue nextTickQueue | ┌─────────────┴─────────────┐ | │ poll │ │ └─────────────┬─────────────┘ │ nextTickQueue │ ┌─────────────┴─────────────┐ │ │ check │ │ └─────────────┬─────────────┘ │ nextTickQueue │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘
下圖補充了官方並無說起的 Microtasks
微任務:
微任務會在主線以後和事件循環的每一個階段以後當即執行。
若是您熟悉JavaScript事件循環,那麼應該對微任務不陌生,這些微任務在Node中的工做方式相同。 若是你想從新瞭解事件循環和微任務隊列,請查看此連接(這東西很是底層,慎點)。
在Node領域,微任務是來自如下對象的回調:
process.nextTick()
then()
handlers for resolved or rejected Promises在主線結束後以及事件循環的每一個階段以後,當即運行微任務回調。
resolved的promise.then
回調像微處理同樣執行,就像process.nextTick
同樣。 雖然,若是二者都在同一個微任務隊列中,則將首先執行process.nextTick
的回調。
優先級 process.nextTick
> promise.then
= queueMicrotask
下面例子完整演示了事件循環:
const fs = require('fs'); const logger = require('../common/logger'); const ITERATIONS_MAX = 2; let iteration = 0; const start = Date.now(); const msleep = (i) => { for (let index = 0; Date.now() - start < i; index++) { // do nonthing } } Promise.resolve().then(() => { // Microtask callback runs AFTER mainline, even though the code is here logger.info('Promise.resolve.then', 'MAINLINE MICROTASK'); }); logger.info('START', 'MAINLINE'); const timeout = setInterval(() => { logger.info('START iteration ' + iteration + ': setInterval', 'TIMERS PHASE'); if (iteration < ITERATIONS_MAX) { setTimeout((iteration) => { logger.info('TIMER EXPIRED (from iteration ' + iteration + '): setInterval.setTimeout', 'TIMERS PHASE'); Promise.resolve().then(() => { logger.info('setInterval.setTimeout.Promise.resolve.then', 'TIMERS PHASE MICROTASK'); }); }, 0, iteration); fs.readdir(__dirname, (err, files) => { if (err) throw err; logger.info('fs.readdir() callback: Directory contains: ' + files.length + ' files', 'POLL PHASE'); queueMicrotask(() => logger.info('setInterval.fs.readdir.queueMicrotask', 'POLL PHASE MICROTASK')); Promise.resolve().then(() => { logger.info('setInterval.fs.readdir.Promise.resolve.then', 'POLL PHASE MICROTASK'); }); }); setImmediate(() => { logger.info('setInterval.setImmediate', 'CHECK PHASE'); Promise.resolve().then(() => { logger.info('setInterval.setTimeout.Promise.resolve.then', 'CHECK PHASE MICROTASK'); }); }); // msleep(1000); // 等待 I/O 完成 } else { logger.info('Max interval count exceeded. Goodbye.', 'TIMERS PHASE'); clearInterval(timeout); } logger.info('END iteration ' + iteration + ': setInterval', 'TIMERS PHASE'); iteration++; }, 0); logger.info('END', 'MAINLINE');
輸出:
1577168519233:INFO: MAINLINE: START 1577168519242:INFO: MAINLINE: END 1577168519243:INFO: MAINLINE MICROTASK: Promise.resolve.then # 第一次 1577168519243:INFO: TIMERS PHASE: START iteration 0: setInterval 1577168519244:INFO: TIMERS PHASE: END iteration 0: setInterval ## 到這裏循環已經結束了 ## 這時候 timers 階段爲空, poll 階段有新事件完成 1577168519245:INFO: POLL PHASE: fs.readdir() callback: Directory contains: 2 files 1577168519245:INFO: POLL PHASE MICROTASK: setInterval.fs.readdir.queueMicrotask 1577168519245:INFO: POLL PHASE MICROTASK: setInterval.fs.readdir.Promise.resolve.then ## 在 poll 階段結束後立刻處理微任務 ## poll 轉 check 階段執行 setImmediate 設置的回調 1577168519245:INFO: CHECK PHASE: setInterval.setImmediate 1577168519245:INFO: CHECK PHASE MICROTASK: setInterval.setTimeout.Promise.resolve.then ## 開始新的循環, timers 隊列不爲空 1577168519246:INFO: TIMERS PHASE: TIMER EXPIRED (from iteration 0): setInterval.setTimeout 1577168519246:INFO: TIMERS PHASE MICROTASK: setInterval.setTimeout.Promise.resolve.then # 第二次 1577168519246:INFO: TIMERS PHASE: START iteration 1: setInterval 1577168519246:INFO: TIMERS PHASE: END iteration 1: setInterval 1577168519246:INFO: CHECK PHASE: setInterval.setImmediate 1577168519246:INFO: CHECK PHASE MICROTASK: setInterval.setTimeout.Promise.resolve.then 1577168519246:INFO: POLL PHASE: fs.readdir() callback: Directory contains: 2 files 1577168519253:INFO: POLL PHASE MICROTASK: setInterval.fs.readdir.queueMicrotask 1577168519253:INFO: POLL PHASE MICROTASK: setInterval.fs.readdir.Promise.resolve.then 1577168519253:INFO: TIMERS PHASE: TIMER EXPIRED (from iteration 1): setInterval.setTimeout 1577168519253:INFO: TIMERS PHASE MICROTASK: setInterval.setTimeout.Promise.resolve.then # 第三次退出 1577168519253:INFO: TIMERS PHASE: START iteration 2: setInterval 1577168519253:INFO: TIMERS PHASE: Max interval count exceeded. Goodbye. 1577168519253:INFO: TIMERS PHASE: END iteration 2: setInterval
運行結果的順序不固定,由於fs.readdir
須要I/O
系統調用,須要等待系統的調度,所以等待事件並不固定。
可是順序仍然是有規律的:
setTimeout
和setImmediate
在timers
階段(不是mainline
就行)被調用,所以setImmediate
老是比setTimeout
快(前面第5節已說明)由於poll
階段等待系統調用的時間不肯定。所以它會在上面二者之間插空,就是3種排序
poll
check
timers
這種可能比較少,取決於I/O
調用速度與進程在當前timers
階段的處理時間——也就是I/O
的事件循環進入poll
階段前就已經完成,也就是poll
隊列不爲空。把上面的msleep
註釋打開便可測試。check
poll
timers
這種狀況比較多出現。check
timers
poll
這種狀況也多。所以存在3種順序。
本文下方連接包含更多例子
timers
階段和poll
階段,由於依賴系統的調度,因此具體在哪一次事件循環執行?這是不肯定的,有多是下次循環就能夠,也許須要等待。在上面彩色圖的事件循環中黃色標記的階段中,只剩下check
階段是肯定的 —— 必然是在本次(還沒到本次循環的check階段的話)或者下次循環調用。還有的是, 微服務是可以保證,必然在本階段結束後下階段前執行。
timers 不肯定,poll 不肯定,check 肯定,Microtasks肯定。
事件是應用程序中發生的重要事件。 諸如Node之類的事件驅動的運行時在某些地方發出事件,並在其餘地方響應事件。
例子:
// The Node EventEmitter const EventEmitter = require('events'); // Create an instance of EventEmitter const eventEmitter = new EventEmitter(); // The common logger const logger = require('../common/logger'); logger.info('START', 'MAINLINE'); logger.info('Registering simpleEvent handler', 'MAINLINE'); eventEmitter.on('simpleEvent', (eventName, message, source, timestamp) => { logger.info('Received event: ' + timestamp + ': ' + source + ':[' + eventName + ']: ' + message, 'EventEmitter.on()'); }); // Get the current time let hrtime = process.hrtime(); eventEmitter.emit('simpleEvent', 'simpleEvent', 'Custom event says what?', 'MAINLINE', (hrtime[0] * 1e9 + hrtime[1] ) / 1e6); logger.info('END', 'MAINLINE');
輸出:
$ node example7 1530379926998:INFO: MAINLINE: START 1530379927000:INFO: MAINLINE: Registering simpleEvent handler 1530379927000:INFO: EventEmitter.on(): Received event: 553491474.966337: MAINLINE:[simpleEvent]: Custom event says what? 1530379927000:INFO: MAINLINE: END
上面結果看出, Event
是同步, 何時emit 就何時執行回調。
這些資料是經過必應國際版
搜索出來,百度不給力。
Phases of the Node JS Event Loop