譯者注:前端
- 爲何要翻譯?其實在翻譯這篇文章前,筆者有Google了一下中文翻譯,看的不是很明白,因此纔有本身翻譯的打算,固然能力有限,文中或有錯漏,歡迎指正。
- 文末會有幾個小問題,你們不妨一塊兒思考一下
- 若是你對NodeJs系列感興趣,歡迎關注微信公衆號:前端神盾局或 github NodeJs系列文章
儘管JavaScript是單線程的,經過Event Loop使得NodeJs可以儘量的經過卸載I/O操做到系統內核,來實現非阻塞I/O的功能。node
因爲大部分現代系統內核都是多線程的,所以他們能夠在後臺執行多個操做。當這些操做中的某一個完成後,內核便會通知NodeJs,這樣(這個操做)指定的回調就會添加到poll
隊列以便最終執行。關於這個咱們會在隨後的章節中進一步說明。git
當NodeJs啓動時,event loop 隨即會被初始化,然後會執行對應的輸入腳本(直接把腳本放入REPL執行不在本文討論範圍內),這個過程當中(腳本的執行)可能會存在對異步API的調用,產生定時器或者調用process.nextTick()
,接着開始event loop。github
譯者注:這段話的意思是NodeJs優先執行同步代碼,在同步代碼的執行過程當中可能會調用到異步API,當同步代碼和
process.nextTick()
回調執行完成後,就會開始event loop
下圖簡要的概述了event loop的操做順序:npm
┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘
注:每個框表明event loop中的一個階段
每一個階段都有一個FIFO(先進先出)的回調隊列等待執行。雖然每一個階段都有其獨特之處,但整體而言,當event loop進入到指定階段後,它會執行該階段的任何操做,並執行對應的回調直到隊列中沒有可執行回調或者達到回調執行上限,然後event loop會進入下一階段。api
因爲任何這些階段的操做可能產生更多操做,內核也會將新的事件推入到poll階段的隊列中,因此新的poll事件被容許在處理poll事件時繼續加入隊,這也意味着長時間運行的回調能夠容許poll階段運行的時間比計時器的閾值要長瀏覽器
注意:Windows和Unix/Linux在實現上有些差異,但這對本文並不重要。事實上存在7到8個步驟,但以上列舉的是Node.js中實際使用的。
setTimeout()
和setInterval()
的回調setImmediate()
設定的回調以外的幾乎全部回調setImmediate
回調在這裏觸發socket.on('close', ...)
在每次執行完event loop後,Node.js都會檢查是否還有須要等待的I/O或者定時器沒有處理,若是沒有那麼進程退出。微信
一個定時器會指定閥值,並在達到閥值以後執行給定的回調,但一般來講這個閥值會超過咱們預期的時間。定時器回調會盡量早的執行,不過操做系統的調度和其餘回調的執行時間會形成必定的延時。多線程
注:嚴格意義上說,定時器何時執行取決於poll階段
舉個例子,假定一個定時器給定的閥值是100ms,異步讀取文件須要95ms的時間異步
const fs = require('fs'); function someAsyncOperation(callback) { // 假定這裏花費了95ms fs.readFile('/path/to/file', callback); } const timeoutScheduled = Date.now(); setTimeout(function() { const delay = Date.now() - timeoutScheduled; console.log(delay + 'ms have passed since I was scheduled'); }, 100); // 95ms後異步操做才完成 someAsyncOperation(function() { const startCallback = Date.now(); // 這裏花費了10ms while (Date.now() - startCallback < 10) { // do nothing } });
就本例而言,當event loop到達poll階段,它的隊列是空的(fs.readFile()
還未完成),所以它會停留在這裏直到達到最先的定時器閥值。fs.readFile()
花費了95ms讀取文件,以後它的回調被推入poll隊列並執行(執行花了10ms)。回調執行完畢後,隊列中已經沒有其餘回調須要執行了,那麼event loop就會去檢查是否有定時器的回調能夠執行,若是有就跳回到timer階段執行相應回調。在本例中,你能夠看到從定時器被調用到其回調被執行一共耗時105ms。
注:爲了防止event loop一直阻塞在poll階段,libuv( http://libuv.org/ 這是用c語言實現了Node.js event loop以及各個平臺的異步行爲的庫)會指定一個硬性的最大值以阻止更多的事件被推入poll。
這個階段用於執行一些系統操做的回調,好比TCP錯誤。舉個例子,當一個TCP socket 在嘗試鏈接時接收到ECONNREFUSED
的錯誤,一些*nix系統會想要獲得這些錯誤的報告,而這都會被推到 I/O callbacks中執行。
poll階段有兩個功能:
當event loop進入到poll階段且此代碼中爲設定定時器,將會發生下面狀況:
若是poll隊列是空的,將會發生下面狀況:
setImmediate()
的調用,event loop將會結束poll階段進入check階段並執行這些已被調度的代碼setImmediate()
的調用,那麼event loop將阻塞在這裏直到有回調被添加進來,新加的回調將會被當即執行一旦poll隊列爲空,event loop就會檢查是否有定時器達到閥值,若是有1個或多個定時器符合要求,event loop將將會回到timers階段並執行改階段的回調.
一旦poll階段完成,本階段的回調將被當即執行。若是poll階段處於空閒狀態而且腳本中有執行了setImmediate()
,那麼event loop會跳過poll階段的等待進入本階段。
實際上setImmediate()
是一個特殊的定時器,它在事件循環的一個單獨階段運行,它使用libuv API來調度執行回調。
一般而言,隨着代碼的執行,event loop最終會進入poll階段並在這裏等待新事件的到來(例如新的鏈接和請求等等)。可是,若是存在setImmediate()
的回調而且poll階段是空閒的,那麼event loop就會中止在poll階段漫無目的的等等直接進入check階段。
若是一個socket或者handle忽然關閉(好比:socket.destory()
),close
事件就會被提交到這個階段。不然它將會經過process.nextTick()
觸發
setImmediate
和setTimeout()
看起來是比較類似,但它們有不一樣的行爲,這取決於它們何時被調用。
setImmediate()
被設計成一旦完成poll階段就會被當即調用setTimeout()
則是在達到最小閥值是纔會被觸發執行其兩者的調用順序取決於它們的執行上下文。若是二者都在主模塊被調用,那麼其回調被執行的時間點就取決於處理過程的性能(這可能被運行在同一臺機器上的其餘應用影響)
好比說,若是下列腳本不是在I/O循環中運行,這兩種定時器運行的順序是不必定的(==這是爲何?==),這取決於處理過程的性能:
// timeout_vs_immediate.js setTimeout(function timeout() { console.log('timeout'); }, 0); setImmediate(function immediate() { console.log('immediate'); });
$ node timeout_vs_immediate.js timeout immediate $ node timeout_vs_immediate.js immediate timeout
可是若是你把上面的代碼置於I/O循環中,setImmediate回調會被優先執行:
// 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()
的主要好處是:若是代碼是在I/O循環中調用,那麼setImmediate()
老是優先於其餘定時器(不管有多少定時器存在)
process.nextTick()
你可能已經注意到process.nextTick()
不在上面的圖表中,即便它也是異步api。這是由於嚴格意義上來講process.nextTick()
不屬於event loop中的一部分,它會忽略event loop當前正在執行的階段,而直接處理nextTickQueue
中的內容。
回過頭看一下圖表,你在任何給定階段調用process.nextTick()
,在繼續event loop以前,全部傳入process.nextTick()
的回調都會被執行。這可能會致使一些很差的狀況,由於它容許你遞歸調用process.nextTick()
從而使得event loop沒法進入poll階段,致使沒法接收到新的 I/O事件
那爲何像這樣的東西會被囊括在Node.js?部分因爲Node.js的設計理念:API應該始終是異步的即便有些地方是不必的。舉個例子:
function apiCall(arg, callback) { if (typeof arg !== 'string') return process.nextTick(callback, new TypeError('argument should be string')); }
這是一段用於參數校驗的代碼,若是參數不正確就會把錯誤信息傳遞到回調。最近process.nextTick()
有進行一些更新,使得咱們能夠傳遞多個參數到回調中而不用嵌套多個函數。
咱們(在這個例子)所作的是在保證了其他(同步)代碼的執行完成後把錯誤傳遞給用戶。經過使用process.nextTick()
咱們能夠確保apiCall()
的回調老是在其餘(同步)代碼運行完成後event loop
開始前調用的。爲了實現這一點,JS調用棧被展開(==什麼是棧展開?==)而後當即執行提供的回調,那咱們就能夠對process.nextTick進行遞歸(==怎麼作到的?==)調用而不會觸發RangeError: Maximum call stack size exceeded from 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()
(函數名能夠看出),但實際上操做是同步的。當它被調用時,其回調也在event loop中的同一階段被調用了,由於someAsyncApiCall()
實際上並無任何異步動做。結果,在(同步)代碼尚未所有執行的時候,回調就嘗試去訪問變量bar
。
經過把回調置於process.nextTick()
,腳本就能完整運行(同步代碼所有執行完畢),這就使得變量、函數等能夠先於回調執行。同時它也有阻止event loop繼續執行的好處。有時候咱們可能但願在event loop繼續執行前拋出一個錯誤,這種狀況下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()
會在隨後的迭代中執行本質上,這兩個的名字應該互換一下,process.nextTick()
比setImmediate()
更接近於當即,可是因爲歷史緣由這不太可能去改變。名字互換可能影響大部分的npm包,天天都有大量的包在提交,這意味這越到後面,互換形成的破壞越大。因此即便它們的名字讓人困惑也不可能被改變。
咱們建議開發者在全部狀況中使用setImmediate()
,由於這可讓你的代碼兼容更多的環境好比瀏覽器。
這裏又兩個主要的緣由:
下面這個例子會知足咱們的指望:
const server = net.createServer(); server.on('connection', function(conn) { }); server.listen(8080); server.on('listening', function() { });
假設listen()
是在event loop開始前運行,可是監聽回調是包裹在setImmediate
中,除非指定hostname參數不然端口將被當即綁定(listening
回調被觸發),event loop必需要執行到poll階段纔會去處理,這意味着存在一種可能:在listening
事件的回調執行前就收到了一個鏈接,也就是至關於先於listening
觸發了connection
事件。
另外一個例子是運行一個繼承至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', function() { console.log('an event occurred!'); });
你沒法當即從構造函數中真正觸發事件,由於腳本尚未運行到用戶爲該事件分配回調的位置。所以,在構造函數中,您可使用 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(function() { this.emit('event'); }.bind(this)); } util.inherits(MyEmitter, EventEmitter); const myEmitter = new MyEmitter(); myEmitter.on('event', function() { console.log('an event occurred!'); });
翻譯完本文,筆者給本身提了幾個問題?
setTimeout
和setImmediate
的執行順序是不必定的?process.nextTick()
能夠被遞歸調用?筆者將在以後的文章《Q&A之理解NodeJs中的Event Loop、Timers以及process.nextTick()》探討這些問題,有興趣的同窗能夠關注筆者的公衆號: 前端情報局-NodeJs系列獲取最新情報
原文地址: https://github.com/nodejs/nod...