摘要:前言以異步和事件驅動的特性著稱但異步是怎么實現(xiàn)的呢其中核心的一部分就是下文中內容基本來自于文檔有不準確地方請指出什么是能讓的操作表現(xiàn)得無阻塞盡管是單線程的但通過盡可能的將操作放到操作系統(tǒng)內核由于現(xiàn)在大多數(shù)內核都是多線程的它們可以在后臺執(zhí)行多
前言
Node.js以異步I/O和事件驅動的特性著稱,但異步I/O是怎么實現(xiàn)的呢?其中核心的一部分就是event loop,下文中內容基本來自于Node.js文檔,有不準確地方請指出.
什么是Event loopevent loop能讓Node.js的I/O操作表現(xiàn)得無阻塞,盡管JavaScript是單線程的但通過盡可能的將操作放到操作系統(tǒng)內核.
由于現(xiàn)在大多數(shù)內核都是多線程的,它們可以在后臺執(zhí)行多個操作. 當這些操作完成時,內核通知Node.js應該把回調函數(shù)添加到poll隊列被執(zhí)行.我們將在接下來的話題里詳細討論.
Event Loop 說明當Node.js開始時,它將會初始化event loop,處理提供可能造成異步API調用,timers任務,或調用process.nextTick()的腳本(或者將它放到[REPL][]中,這篇文章中將不會討論),然后開始處理event loop.
下面是一張event loop操作的簡單概覽圖.
┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘
注意: 每一個方框將被簡稱為一個event loop的階段.
每一個階段都有一個回調函數(shù)的FIFO隊列被執(zhí)行.每一個階段都有自己特有的方式,通常even loop進入一個給定的階段時,它將執(zhí)行該階段任何的特定操作,然后執(zhí)行該階段隊列中的回調函數(shù),直到執(zhí)行完所有回調或執(zhí)行了最大回調的次數(shù).當隊列中的回調已被執(zhí)行完或者到達了限制次數(shù),eventloop將會從下一個階段開始依次執(zhí)行.
由于這些操作可能造成更多的操作,并且在poll階段中產生的新事件被內核推入隊列,所以poll事件可以被推入隊列當有其它poll事件正在執(zhí)行時.因此長時間執(zhí)行回調可以允許poll階段超過timers設定的時間.詳細內容請看timers和poll章節(jié).
ps: 個人理解-在輪詢階段一個回調執(zhí)行可能會產生新的事件處理,這些新事件會被推入到輪詢隊列中,所以poll階段可以一直執(zhí)行回調,即使timers的回調已到時間應該被執(zhí)行時.
注意: Windows和Unix/Linux在實現(xiàn)時有一些細微的差異,但那都不是事兒.重點是: 實際上有7或8個步驟,Node.js實際上使用的是它們所有.
階段概覽timers: 這個階段執(zhí)行setTimeout()和 setInterval()產生的回調.
I/O callbacks: 執(zhí)行大多數(shù)的回調,除了close callbacks,timers和setImmediate()的回調.
idle, prepare: 僅供內部使用.
poll: 獲取新的I/O事件;node會在適當時候在這里阻塞.
check: 執(zhí)行setImmediate()回調.
close callbacks: e.g. socket.on("close", ...).
在每次event loop之間,Node.js會檢查它是否正在等待任何異步I/O或計時器,如果沒有就會完全關閉.
階段詳情 timers一個定時器指定的是執(zhí)行回調函數(shù)的閾值,而不是確定的時間點.定時器的回調將在規(guī)定的時間過后運行;然而,操作系統(tǒng)調度或其他回調函數(shù)的運行可能會使執(zhí)行回調延遲.
注意: 技術上,poll 階段控制了timers被執(zhí)行.
例如, 你要在100ms的延時后在回調函數(shù)并且執(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(function() { 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(function() { const startCallback = Date.now(); // do something that will take 10ms... while (Date.now() - startCallback < 10) { // do nothing } }); // 輸出: 105ms have passed since I was scheduled
當event loop進入poll階段時,它是一個空的隊列(fs.readFile()還沒有完成),所以它會等待數(shù)毫秒等待timers設定時間的到達.直到等待95 ms過后, fs.readFile()完成文件讀取然后它的回調函數(shù)會被添加至poll隊列然后執(zhí)行.當執(zhí)行完成后隊列中沒有其他回調,所以event loop會查看定時器設定的時間已經到達然后回撤到timers階段執(zhí)行timers的回調函數(shù).在例子里你會發(fā)現(xiàn),從定時器被記錄到執(zhí)行回調函數(shù)耗時105ms.
注意: 為了防止poll階段阻塞死event loop, [libuv]
(http://libuv.org/) (實現(xiàn)Node.js事件循環(huán)的C庫和平臺的所有異步行為)
也有一個固定最大值(系統(tǒng)依賴).
這個階段執(zhí)行一些系統(tǒng)操作的回調,例如TCP錯誤等類型.例如TCP socket 嘗試連接時收到了ECONNREFUSED,一些*nix系統(tǒng)想等待錯誤日志記錄.這些都將在I/O callbacks階段被推入隊列執(zhí)行.
pollpoll 階段有兩個主要的功能:
為已經到達或超時的定時器執(zhí)行腳本
處理在poll隊列中的事件.
當event loop進入poll階段并且沒有timers任務時會執(zhí)行下面某一條操作:
如果poll隊列不為空,則event loop會同步的執(zhí)行回調隊列,直到執(zhí)行完回調或達到系統(tǒng)最大限制.
如果poll隊列為空,會執(zhí)行下面某一條操做:
如果腳本被setImmediate()執(zhí)行,則event loop會結束 poll階段,繼續(xù)向下進入到check階段執(zhí)行setImmediate()的腳本.
如果腳本不是被setImmediate()執(zhí)行,event loop會等待回調函數(shù)被添加至隊列,然后立刻執(zhí)行它們.
一旦poll隊列空了,event loop會檢查timers是否有以滿足條件的定時器,如果有一個以上滿足執(zhí)行條件的定時器,event loop將會撤回至timers階段去執(zhí)行定時器的回調函數(shù).
check這個階段允許立刻執(zhí)行一個回調在poll階段完成后.如果poll階段已經執(zhí)行完成或腳本已經使用setImmediate(),event loop 可能就會繼續(xù)到check階段而不是等待.
setImmediate()實際是在event loop 獨立階段運行的特殊定時器.它使用了libuv API來使回調函數(shù)在poll階段后執(zhí)行.
通常在代碼執(zhí)行時,event loop 最終會到達poll階段,等待傳入連接,請求等等.然而,如果有一個被setImmediate()執(zhí)行的回調,poll階段會變得空閑,它將會結束并進入check階段而不是等待新的poll事件.
close callbacks如果一個socket或者操作被突然關閉(例如.socket.destroy()),這個close事件將在這個階段被觸發(fā).否則它將會通過process.nextTick()被觸發(fā).
setImmediate() vs setTimeout()setImmediate 和 setTimeout() 是很相似的,但是它們的調用方式不同導致了會有不同的表現(xiàn).
setImmediate() 會中斷poll階段,立即執(zhí)行..
setTimeout() 將在給定的毫秒后執(zhí)行設定的腳本.
timers的執(zhí)行順序會根據(jù)它們被調用的上下文而變化.如果兩個都在主模塊內被調用,則時序將受到進程的性能的限制(可能受機器上運行的其他應用程序的影響).
例如,我們執(zhí)行下面兩個不在I/O周期內(主模塊)的腳本,這兩個timers的執(zhí)行順序是不確定的,它受到進程性能的影響:
// 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周期內,則immediate的回調總會被先執(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()的好處是setImmediate()在I/O周期內總是比所有timers先執(zhí)行,無論有多少timers存在.
process.nextTick() 理解 process.nextTick()你可能已經注意到process.nextTick()沒有在概覽圖中列出,盡管他是異步API的一部分.這是因為process.nextTick()在技術上不是event loop的一部分.反而nextTickQueue會在當前操作完成后會被執(zhí)行,無論當前處于event loop的什么階段.
再看看概覽圖,在給定的階段你任何時候調用process.nextTick(),通過process.nextTick()指定的回調函數(shù)都會在event loop繼續(xù)執(zhí)行前被解析.這可能會造成一些不好的情況,因為它允許你通過遞歸調用process.nextTick()而造成I/O阻塞死,因為它阻止了event loop到達poll階段.
為什么這種操作會被允許呢?部分原因是一個API應該是異步事件盡管它可能不是異步的.看看下面代碼片段:
function apiCall(arg, callback) { if (typeof arg !== "string") return process.nextTick(callback, new TypeError("argument should be string")); }
代碼里對參數(shù)做了校驗,如果不正確,它將會在回調函數(shù)中拋出錯誤.API最近更新,允許傳遞參數(shù)給 process.nextTick() ,process.nextTick()可以接受任何參數(shù),回調函數(shù)被當做參數(shù)傳遞給回調函數(shù)后,你就不必使用嵌套函數(shù)了.
我們所做的就是將錯誤回傳給用戶當用戶的其它代碼執(zhí)行后.通過使用process.nextTick()我們確保apiCall()執(zhí)行回調函數(shù)在用戶的代碼之后,在event loop運行的階段之前.為了實現(xiàn)這一點,JS調用的堆棧被允許釋放掉,然后立刻執(zhí)行提供的回調函數(shù),回調允許用戶遞歸的調用process.nextTick()直到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;
用戶定義了一個有異步標簽的函數(shù)someAsyncApiCall(),盡管他的操作是同步的.當它被調用的時候,提供的回調函數(shù)在event loop的同一階段中被調用,因為someAsyncApiCall()沒有任何異步操作.所以回調函數(shù)嘗試引用bar盡管這個變量在作用域沒有值,因為代碼還沒有執(zhí)行到最后.
通過將回調函數(shù)放在process.nextTick()里,代碼仍然有執(zhí)行完的能力,允許所有的變量,函數(shù)等先被初始化來供回調函數(shù)調用.它還有不允許event loop繼續(xù)執(zhí)行的優(yōu)勢.它可能在event loop繼續(xù)執(zhí)行前拋出一個錯誤給用戶很有用.這里提供一個使用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", () => {});
僅當端口可用時端口立即被綁定.所以"listening"的回調函數(shù)能立即被調用.問題是那時候不會設置.on("listening").
為了解決這個問題,"listening"事件被放入nextTick()隊列來允許代碼執(zhí)行完.這會允許用戶設置他們想要的任何事件處理.
process.nextTick() vs setImmediate()我們有兩個直到現(xiàn)在用戶都關心的相似的調用,但他們的名字令人困惑.
process.nextTick() 在同一階段立即觸發(fā)
setImmediate() 在以下迭代器或者event loop的"tick"中觸發(fā)
本質上,這兩個名字應該交換.process.nextTick()比setImmediate()觸發(fā)要快但這是一個不想改變的歷史的命名.做這個改變會破壞npm上大多數(shù)包.每天都有新模塊被增加,意味著每天我們都在等待更多的潛在錯誤發(fā)生.當他們困惑時,這個名字就不會被改變.
我們建議開發(fā)者使用setImmediate()因為它更容易被理解(并且它保持了更好的兼容性,例如瀏覽器的JS)
為什么使用process.nextTick()?有兩個主要原因:
允許用戶處理錯誤,清除任何不需要的資源,或者可能在事件循環(huán)繼續(xù)之前再次嘗試該請求.
同時有必要允許回調函數(shù)執(zhí)行在調用堆棧釋放之后但在event loop繼續(xù)之前.
一個滿足用戶期待的簡單例子:
const server = net.createServer(); server.on("connection", function(conn) { }); server.listen(8080); server.on("listening", function() { });
listen()在event loop開始時執(zhí)行,但是listening的回調函數(shù)被放在一個setImmediate()中.現(xiàn)在除非主機名可用于綁定端口會立即執(zhí)行.現(xiàn)在為了event loop繼續(xù)執(zhí)行,它必須進入poll階段,意味著在監(jiān)聽事件前且沒有觸發(fā)允許連接事件時沒有接收到請求的可能.
另一個例子是運行一個函數(shù)構造函數(shù),例如,繼承自EventEmitter,并且想要在構造函數(shù)中調用一個事件:
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!"); });
你不能在構造函數(shù)中立即觸發(fā)事件,因為代碼不會執(zhí)行到用戶為該事件分配回調函數(shù)的地方,所以,在構造函數(shù)本身中,你可以使用process.nextTick()設置回調函數(shù)來在夠咱函數(shù)完成后觸發(fā)事件.有一個小栗子:
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!"); });部分個人理解
前面基本是基于文檔的翻譯(由于英文能力問題,很多地方都模模糊糊,甚至是狗屁不通[捂臉]),下面寫一些重點部分的理解
幾個概念event loop是跑在主進程上的一個while(true) {}循環(huán).
timers階段包括setTimeout(),setInterval()兩個定時器,回調執(zhí)行時間等于或者晚于定時器設定的時間,因為在poll階段會執(zhí)行其它回調函數(shù),在空閑時才回去檢查定時器(event loop的開始和結束時檢查).
在I/O callback階段,雖然在階段介紹里說的是執(zhí)行除timers,Immediate,close之外的所有回調,但后面詳細介紹中又說了,這里執(zhí)行的大多是stream, pipe, tcp, udp通信錯誤的回調,例如fs產生的回調應該還是在poll階段執(zhí)行的.
poll階段應該才是真正的執(zhí)行了除timers,Immediate,close外的所有回調.
process.nextTick()沒有在任何一個階段執(zhí)行,它執(zhí)行的時間應該是在各個階段切換的中間執(zhí)行.
幾段代碼const fs = require("fs"); fs.readFile("../mine.js", () => { setTimeout(() => { console.log("setTimeout") }, 0); process.nextTick(() => { console.log("process.nextTick") }) setImmediate(() => { console.log("setImmediate") }) }); /*log ------------------- process.nextTick setImmediate setTimeout */
當文件讀取完成后在poll階段執(zhí)行回調函數(shù)
將setTimeout添加至timers隊列,解析process.nextTick()回調函數(shù),將setImmediate添加至check隊列
poll隊列為空,有setImmediate的代碼,繼續(xù)向下一個階段.
在到達check階段前執(zhí)行process.nextTick()回調函數(shù)
在check階段執(zhí)行setImmediate
在timers階段執(zhí)行setTimeout回調
const fs = require("fs"); const start = new Date(); fs.readFile("../mine.js", () => { setTimeout(() => { console.log("setTimeout spend: ", new Date() - start) }, 0); setImmediate(() => { console.log("setImmediate spend: ", new Date() - start) }) process.nextTick(() => { console.log("process.nextTick spend: ", new Date() - start) }) }); setTimeout(() => { console.log("setTimeout-main spend: ", new Date() - start) }, 0); setImmediate(() => { console.log("setImmediate-main spend: ", new Date() - start) }) process.nextTick(() => { console.log("process.nextTick-main spend: ", new Date() - start) }) /* log ---------------- process.nextTick-main spend: 9 setTimeout-main spend: 12 setImmediate-main spend: 13 process.nextTick spend: 14 setImmediate spend: 15 setTimeout spend: 15 */
這里沒有搞懂為什么主進程內的setTimeout總是比setImmediate先執(zhí)行,按文檔所說,兩個應該是不確定誰先執(zhí)行.
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/84886.html
摘要:前沿是基于引擎的運行環(huán)境具有事件驅動非阻塞等特點結合具有網絡編程文件系統(tǒng)等服務端的功能用庫進行異步事件處理線程的單線程含義實際上說的是執(zhí)行同步代碼的主線程一個程序的啟動不止是分配了一個線程,而是我們只能在一個線程執(zhí)行代碼當出現(xiàn)資源調用連接等 前沿 Node.js 是基于V8引擎的javascript運行環(huán)境. Node.js具有事件驅動, 非阻塞I/O等特點. 結合Node API, ...
js異步歷史 一個 JavaScript 引擎會常駐于內存中,它等待著我們把JavaScript 代碼或者函數(shù)傳遞給它執(zhí)行 在 ES3 和更早的版本中,JavaScript 本身還沒有異步執(zhí)行代碼的能力,引擎就把代碼直接順次執(zhí)行了,異步任務都是宿主環(huán)境(瀏覽器)發(fā)起的(setTimeout、AJAX等)。 在 ES5 之后,JavaScript 引入了 Promise,這樣,不需要瀏覽器的安排,J...
摘要:異步在中,是在單線程中執(zhí)行的沒錯,但是內部完成工作的另有線程池,使用一個主進程和多個線程來模擬異步。在事件循環(huán)中,觀察者會不斷的找到線程池中已經完成的請求對象,從中取出回調函數(shù)和數(shù)據(jù)并執(zhí)行。 1. 介紹 單線程編程會因阻塞I/O導致硬件資源得不到更優(yōu)的使用。多線程編程也因為編程中的死鎖、狀態(tài)同步等問題讓開發(fā)人員頭痛。Node在兩者之間給出了它的解決方案:利用單線程,遠離多線程死鎖、狀態(tài)...
摘要:新加了一個微任務和一個宏任務在當前執(zhí)行棧的尾部下一次之前觸發(fā)回調函數(shù)。階段這個階段主要執(zhí)行一些系統(tǒng)操作帶來的回調函數(shù),如錯誤,如果嘗試鏈接時出現(xiàn)錯誤,一些會把這個錯誤報告給。 JavaScript引擎又稱為JavaScript解釋器,是JavaScript解釋為機器碼的工具,分別運行在瀏覽器和Node中。而根據(jù)上下文的不同,Event loop也有不同的實現(xiàn):其中Node使用了libu...
摘要:通過查看的文檔可以發(fā)現(xiàn)整個分為個階段定時器相關任務,中我們關注的是它會執(zhí)行和中到期的回調執(zhí)行某些系統(tǒng)操作的回調內部使用執(zhí)行,一定條件下會在這個階段阻塞住執(zhí)行的回調如果或者關閉了,就會在這個階段觸發(fā)事件,執(zhí)行事件的回調的代碼在文件中。 showImg(https://segmentfault.com/img/bVbd7B7?w=1227&h=644); 這次我們就不要那么多前戲,直奔主題...
閱讀 989·2023-04-26 01:47
閱讀 1674·2021-11-18 13:19
閱讀 2047·2019-08-30 15:44
閱讀 654·2019-08-30 15:44
閱讀 2298·2019-08-30 15:44
閱讀 1236·2019-08-30 14:06
閱讀 1427·2019-08-30 12:59
閱讀 1904·2019-08-29 12:49