摘要:檢索新的事件執(zhí)行與相關(guān)的回調(diào)幾乎所有,除了由定時(shí)器調(diào)度的一些和將在適當(dāng)?shù)臅r(shí)候在這里阻塞。在事件循環(huán)的每次運(yùn)行之間,檢查它是否在等待任何異步或定時(shí)器,如果沒有,則徹底關(guān)閉。
Node.js事件循環(huán)、定時(shí)器和process.nextTick() 什么是事件循環(huán)?
事件循環(huán)允許Node.js執(zhí)行非阻塞I/O操作 — 盡管JavaScript是單線程的 — 通過盡可能將操作卸載到系統(tǒng)內(nèi)核。
由于大多數(shù)現(xiàn)代內(nèi)核都是多線程的,因此它們可以處理在后臺執(zhí)行的多個(gè)操作,當(dāng)其中一個(gè)操作完成時(shí),內(nèi)核會告訴Node.js,以便可以將相應(yīng)的回調(diào)添加到輪詢隊(duì)列中以最終執(zhí)行,我們將在本主題后面進(jìn)一步詳細(xì)解釋。
事件循環(huán)解釋當(dāng)Node.js啟動時(shí),它初始化事件循環(huán),處理提供的可能會進(jìn)行異步API調(diào)用、調(diào)度定時(shí)器或調(diào)用process.nextTick()的輸入腳本(或放入REPL,本文檔未涉及),然后開始處理事件循環(huán)。
下面的圖解顯示了事件循環(huán)操作順序的簡要概述。
┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘
注意:每個(gè)框都將被稱為事件循環(huán)的“階段”。
每個(gè)階段都有一個(gè)要執(zhí)行的回調(diào)FIFO隊(duì)列,雖然每個(gè)階段都以其自己的方式特殊,但通常情況下,當(dāng)事件循環(huán)進(jìn)入給定階段時(shí),它將執(zhí)行特定于該階段的任何操作,然后在該階段的隊(duì)列中執(zhí)行回調(diào),直到隊(duì)列耗盡或已執(zhí)行最大回調(diào)數(shù)。當(dāng)隊(duì)列耗盡或達(dá)到回調(diào)限制時(shí),事件循環(huán)將移至下一階段,依此類推。
由于任何這些操作都可以調(diào)度更多操作,并且在輪詢階段處理的新事件由內(nèi)核排隊(duì),輪詢事件可以在處理輪詢事件時(shí)排隊(duì),因此,長時(shí)間運(yùn)行的回調(diào)可以允許輪詢階段的運(yùn)行時(shí)間遠(yuǎn)遠(yuǎn)超過定時(shí)器的閾值,有關(guān)詳細(xì)信息,請參閱timers和poll部分。
注意:Windows和Unix/Linux實(shí)現(xiàn)之間存在輕微差異,但這對于此示范并不重要,最重要的部分在這里,實(shí)際上有七到八個(gè)步驟,但我們關(guān)心的是 — Node.js實(shí)際使用的那些 — 是上面那些。
階段概述timers:此階段執(zhí)行由setTimeout()和setInterval()調(diào)度的回調(diào)。
pending callbacks:執(zhí)行延遲到下一個(gè)循環(huán)迭代的I/O回調(diào)。
idle, prepare:僅在內(nèi)部使用。
poll:檢索新的I/O事件;執(zhí)行與I/O相關(guān)的回調(diào)(幾乎所有,除了close callbacks、由定時(shí)器調(diào)度的一些和setImmediate());node將在適當(dāng)?shù)臅r(shí)候在這里阻塞。
check:這里調(diào)用setImmediate()回調(diào)函數(shù)。
close callbacks:一些關(guān)閉回調(diào),例如socket.on("close", ...)。
在事件循環(huán)的每次運(yùn)行之間,Node.js檢查它是否在等待任何異步I/O或定時(shí)器,如果沒有,則徹底關(guān)閉。
階段的細(xì)節(jié) timers定時(shí)器指定閾值,在該閾值之后可以執(zhí)行提供的回調(diào)而不是人們希望它執(zhí)行的確切時(shí)間,定時(shí)器回調(diào)將在指定的時(shí)間過后可以調(diào)度,但是,操作系統(tǒng)調(diào)度或其他回調(diào)的運(yùn)行可能會延遲它們。
注意:從技術(shù)上講,輪詢階段控制何時(shí)執(zhí)行定時(shí)器。
例如,假設(shè)你在100毫秒閾值后調(diào)度執(zhí)行超時(shí),那么你的腳本將異步讀取一個(gè)耗時(shí)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)入輪詢階段時(shí),它有一個(gè)空隊(duì)列(fs.readFile()尚未完成),所以它將等待剩余的ms數(shù),直到達(dá)到最快的定時(shí)器閾值,當(dāng)它等待95毫秒通過,fs.readFile()完成了讀取文件,其需要10毫秒完成的回調(diào)被添加到輪詢隊(duì)列并執(zhí)行,當(dāng)回調(diào)結(jié)束時(shí),隊(duì)列中不再有回調(diào),因此事件循環(huán)將看到已達(dá)到最快定時(shí)器的閾值然后回到定時(shí)器階段以執(zhí)行定時(shí)器的回調(diào),在此示例中,你將看到正在調(diào)度的定時(shí)器與正在執(zhí)行的回調(diào)之間的總延遲將為105毫秒。
注意:為了防止輪詢階段耗盡事件循環(huán),libuv(實(shí)現(xiàn)Node.js事件循環(huán)的C庫以及平臺的所有異步行為)在停止輪詢更多事件之前,還具有硬性最大值(取決于系統(tǒng))。
pending callbacks此階段執(zhí)行某些系統(tǒng)操作(例如TCP錯(cuò)誤類型)的回調(diào),例如,如果TCP socket在嘗試連接時(shí)收到ECONNREFUSED,某些*nix系統(tǒng)要等待報(bào)告錯(cuò)誤,這將在等待回調(diào)階段排隊(duì)執(zhí)行。
poll輪詢階段有兩個(gè)主要功能:
計(jì)算它應(yīng)該阻塞和輪詢I/O的時(shí)間。
然后處理輪詢隊(duì)列中的事件。
當(dāng)事件循環(huán)進(jìn)入輪詢階段并且沒有定時(shí)器被調(diào)度時(shí),將發(fā)生以下兩種情況之一:
如果輪詢隊(duì)列不為空,則事件循環(huán)將遍歷其同步執(zhí)行它們的回調(diào)隊(duì)列,直到隊(duì)列已用盡,或者達(dá)到系統(tǒng)相關(guān)的硬限制。
如果輪詢隊(duì)列為空,則會發(fā)生以下兩種情況之一:
如果setImmediate()已調(diào)度腳本,則事件循環(huán)將結(jié)束輪詢階段并繼續(xù)執(zhí)行檢查階段以執(zhí)行這些調(diào)度腳本。
如果setImmediate()尚未調(diào)度腳本,則事件循環(huán)將等待將回調(diào)添加到隊(duì)列,然后立即執(zhí)行它們。
輪詢隊(duì)列為空后,事件循環(huán)將檢查已達(dá)到時(shí)間閾值的定時(shí)器,如果一個(gè)或多個(gè)定時(shí)器準(zhǔn)備就緒,事件循環(huán)將回繞到定時(shí)器階段以執(zhí)行那些定時(shí)器的回調(diào)。
check此階段允許人員在輪詢階段完成后立即執(zhí)行回調(diào),如果輪詢階段變?yōu)榭臻e并且腳本已使用setImmediate()排隊(duì),則事件循環(huán)可以繼續(xù)到檢查階段而不是等待。
setImmediate()實(shí)際上是一個(gè)特殊的定時(shí)器,它在事件循環(huán)的一個(gè)多帶帶階段運(yùn)行,它使用libuv API來調(diào)度在輪詢階段完成后執(zhí)行回調(diào)。
通常,在執(zhí)行代碼時(shí),事件循環(huán)最終將進(jìn)入輪詢階段,在此階段它將等待傳入連接、請求等,但是,如果已使用setImmediate()調(diào)度回調(diào)并且輪詢階段變?yōu)榭臻e,則它將結(jié)束并繼續(xù)到檢查階段,而不是等待輪詢事件。
close callbacks如果socket或handle突然關(guān)閉(例如socket.destroy()),則在此階段將發(fā)出"close"事件,否則它將通過process.nextTick()發(fā)出。
setImmediate()與setTimeout()setImmediate()和setTimeout()類似,但行為方式不同,取決于他們何時(shí)被調(diào)用。
setImmediate()用于在當(dāng)前輪詢階段完成后執(zhí)行腳本。
setTimeout()調(diào)度在經(jīng)過最小閾值(以ms為單位)后運(yùn)行腳本。
執(zhí)行定時(shí)器的順序?qū)⒏鶕?jù)調(diào)用它們的上下文而有所不同,如果從主模塊中調(diào)用兩者,則時(shí)間將受到進(jìn)程性能的限制(可能受到計(jì)算機(jī)上運(yùn)行的其他應(yīng)用程序的影響)。
例如,如果我們運(yùn)行不在I/O周期內(nèi)的以下腳本(即主模塊),則執(zhí)行兩個(gè)定時(shí)器的順序是不確定的,因?yàn)樗苓M(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
但是,如果移動兩個(gè)調(diào)用到I/O周期內(nèi),則始終首先執(zhí)行immediate回調(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)點(diǎn)是setImmediate()將始終在任何定時(shí)器之前執(zhí)行(如果在I/O周期內(nèi)調(diào)度),與存在多少定時(shí)器無關(guān)。
process.nextTick() 理解process.nextTick()你可能已經(jīng)注意到,process.nextTick()沒有顯示在圖解中,即使它是異步API的一部分,這是因?yàn)?b>process.nextTick()在技術(shù)上不是事件循環(huán)的一部分,相反,nextTickQueue將在當(dāng)前操作完成后處理,而不管事件循環(huán)的當(dāng)前階段如何。
回顧一下我們的圖解,無論何時(shí)在給定階段調(diào)用process.nextTick(),傳遞給process.nextTick()的所有回調(diào)都將在事件循環(huán)繼續(xù)之前得到解決,這可能會產(chǎn)生一些糟糕的情況,因?yàn)樗试S你通過進(jìn)行遞歸process.nextTick()調(diào)用來“餓死”你的I/O,這會阻止事件循環(huán)到達(dá)輪詢階段。
為什么會被允許?為什么這樣的東西會被包含在Node.js中?其中一部分是一種設(shè)計(jì)理念,其中API應(yīng)該始終是異步的,即使它不是必須的,以此代碼段為例:
function apiCall(arg, callback) { if (typeof arg !== "string") return process.nextTick(callback, new TypeError("argument should be string")); }
該片段進(jìn)行參數(shù)檢查,如果它不正確,它會將錯(cuò)誤傳遞給回調(diào),最近更新的API允許將參數(shù)傳遞給process.nextTick(),允許它將回調(diào)后傳遞的任何參數(shù)作為參數(shù)傳播到回調(diào),因此你不必嵌套函數(shù)。
我們正在做的是將錯(cuò)誤傳回給用戶,但只有在我們允許其余的用戶代碼執(zhí)行之后,通過使用process.nextTick(),我們保證apiCall()始終在用戶代碼的其余部分之后并且在允許事件循環(huán)之前運(yùn)行其回調(diào),為了實(shí)現(xiàn)這一點(diǎn),JS調(diào)用堆棧允許放松然后立即執(zhí)行提供的回調(diào),這允許一個(gè)人對process.nextTick()進(jìn)行遞歸調(diào)用而不會達(dá)到RangeError: Maximum call stack size exceeded from v8。
這種理念可能會導(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()定義為具有異步簽名,但它實(shí)際上是同步操作的,當(dāng)它被調(diào)用時(shí),提供給someAsyncApiCall()的回調(diào)在事件循環(huán)的同一階段被調(diào)用,因?yàn)?b>someAsyncApiCall()實(shí)際上不會異步執(zhí)行任何操作。因此,回調(diào)嘗試引用bar,即使它在范圍內(nèi)可能沒有該變量,因?yàn)樵撃_本無法運(yùn)行完成。
通過將回調(diào)放在process.nextTick()中,腳本仍然能夠運(yùn)行完成,允許所有變量、函數(shù)等,在調(diào)用回調(diào)之前進(jìn)行初始化。它還具有不允許事件循環(huán)繼續(xù)的優(yōu)點(diǎn),在允許事件循環(huán)繼續(xù)之前,向用戶警告錯(cuò)誤可能是有用的,以下是使用process.nextTick()的前一個(gè)示例:
let bar; function someAsyncApiCall(callback) { process.nextTick(callback); } someAsyncApiCall(() => { console.log("bar", bar); // 1 }); bar = 1;
這是另一個(gè)真實(shí)世界的例子:
const server = net.createServer(() => {}).listen(8080); server.on("listening", () => {});
僅傳遞端口時(shí),端口立即綁定,因此,可以立即調(diào)用"listening"回調(diào),問題是那時(shí)候不會設(shè)置.on("listening")回調(diào)。
為了解決這個(gè)問題,"listening"事件在nextTick()中排隊(duì),以允許腳本運(yùn)行完成,這允許用戶設(shè)置他們想要的任何事件處理程序。
process.nextTick() vs setImmediate()就用戶而言,我們有兩個(gè)類似的調(diào)用,但它們的名稱令人困惑。
process.nextTick()在同一階段立即觸發(fā)。
setImmediate()在事件循環(huán)的后續(xù)迭代或"tick"觸發(fā)。
實(shí)質(zhì)上,應(yīng)該交換名稱,process.nextTick()比setImmediate()更快地觸發(fā),但這是過去的一個(gè)工件,不太可能改變。進(jìn)行此切換會破壞npm上的大部分包,每天都會添加更多新模塊,這意味著我們每天都在等待更多潛在的破損,雖然它們令人困惑,但名稱本身不會改變。
我們建議開發(fā)人員在所有情況下都使用setImmediate(),因?yàn)樗菀淄评恚ú⑶宜勾a與更廣泛的環(huán)境兼容,如瀏覽器JS)。
為什么要使用process.nextTick()?主要有兩個(gè)原因:
允許用戶處理錯(cuò)誤、清除任何不需要的資源,或者在事件循環(huán)繼續(xù)之前再次嘗試請求。
有時(shí),在調(diào)用堆棧已解除但在事件循環(huán)繼續(xù)之前,必須允許回調(diào)運(yùn)行。
一個(gè)例子是匹配用戶的期望,簡單的例子:
const server = net.createServer(); server.on("connection", (conn) => { }); server.listen(8080); server.on("listening", () => { });
假設(shè)listen()在事件循環(huán)開始時(shí)運(yùn)行,但是監(jiān)聽回調(diào)放在setImmediate()中,除非傳遞主機(jī)名,否則將立即綁定到端口。要使事件循環(huán)繼續(xù),它必須達(dá)到輪詢階段,這意味著有一個(gè)非零的可能性,連接可能已經(jīng)被接收,允許連接事件在監(jiān)聽事件之前被觸發(fā)。
另一個(gè)例子是運(yùn)行一個(gè)函數(shù)構(gòu)造函數(shù),比如繼承自EventEmitter,它想在構(gòu)造函數(shù)中調(diào)用一個(gè)事件:
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ā)出事件,因?yàn)槟_本還沒有處理到用戶為該事件分配回調(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!"); });上一篇:阻塞與非阻塞概述 下一篇:不要阻塞事件循環(huán)(或工作池)
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/108572.html
摘要:中的定時(shí)器中的模塊包含在一段時(shí)間后執(zhí)行代碼的函數(shù),定時(shí)器不需要通過導(dǎo)入,因?yàn)樗蟹椒ǘ伎梢栽谌址秶鷥?nèi)模擬瀏覽器,要完全了解何時(shí)執(zhí)行定時(shí)器功能,最好先閱讀事件循環(huán)。 Node.js中的定時(shí)器 Node.js中的Timers模塊包含在一段時(shí)間后執(zhí)行代碼的函數(shù),定時(shí)器不需要通過require()導(dǎo)入,因?yàn)樗蟹椒ǘ伎梢栽谌址秶鷥?nèi)模擬瀏覽器JavaScript API,要完全了解何時(shí)執(zhí)行定...
摘要:回調(diào)函數(shù)執(zhí)行幾乎所有的回調(diào)函數(shù),除了關(guān)閉回調(diào)函數(shù),定時(shí)器計(jì)劃的回調(diào)函數(shù)和。輪詢此階段有兩個(gè)主要的功能執(zhí)行已過時(shí)的定時(shí)器腳本處理輪詢隊(duì)列中的事件。一旦輪詢隊(duì)列為空,事件循環(huán)將檢查已達(dá)到時(shí)間閾值的定時(shí)器。 什么是事件循環(huán)(Event Loop)? 事件環(huán)使得Node.js可以執(zhí)行非阻塞I/O 操作,只要有可能就將操作卸載到系統(tǒng)內(nèi)核,盡管JavaScript是單線程的。 由于大多數(shù)現(xiàn)代(終端...
摘要:前沿是基于引擎的運(yùn)行環(huán)境具有事件驅(qū)動非阻塞等特點(diǎn)結(jié)合具有網(wǎng)絡(luò)編程文件系統(tǒng)等服務(wù)端的功能用庫進(jìn)行異步事件處理線程的單線程含義實(shí)際上說的是執(zhí)行同步代碼的主線程一個(gè)程序的啟動不止是分配了一個(gè)線程,而是我們只能在一個(gè)線程執(zhí)行代碼當(dāng)出現(xiàn)資源調(diào)用連接等 前沿 Node.js 是基于V8引擎的javascript運(yùn)行環(huán)境. Node.js具有事件驅(qū)動, 非阻塞I/O等特點(diǎn). 結(jié)合Node API, ...
摘要:異步在中,是在單線程中執(zhí)行的沒錯(cuò),但是內(nèi)部完成工作的另有線程池,使用一個(gè)主進(jìn)程和多個(gè)線程來模擬異步。在事件循環(huán)中,觀察者會不斷的找到線程池中已經(jīng)完成的請求對象,從中取出回調(diào)函數(shù)和數(shù)據(jù)并執(zhí)行。 1. 介紹 單線程編程會因阻塞I/O導(dǎo)致硬件資源得不到更優(yōu)的使用。多線程編程也因?yàn)榫幊讨械乃梨i、狀態(tài)同步等問題讓開發(fā)人員頭痛。Node在兩者之間給出了它的解決方案:利用單線程,遠(yuǎn)離多線程死鎖、狀態(tài)...
摘要:令人困惑的是,文檔中稱,指定的回調(diào)函數(shù),總是排在前面。另外,由于指定的回調(diào)函數(shù)是在本次事件循環(huán)觸發(fā),而指定的是在下次事件循環(huán)觸發(fā),所以很顯然,前者總是比后者發(fā)生得早,而且執(zhí)行效率也高因?yàn)椴挥脵z查任務(wù)隊(duì)列。 一、定時(shí)器 除了放置異步任務(wù)的事件,任務(wù)隊(duì)列還可以放置定時(shí)事件,即指定某些代碼在多少時(shí)間之后執(zhí)行。這叫做定時(shí)器(timer)功能,也就是定時(shí)執(zhí)行的代碼。 定時(shí)器功能主要由setTim...
閱讀 2267·2021-09-26 09:55
閱讀 3600·2021-09-23 11:22
閱讀 2163·2019-08-30 15:54
閱讀 1908·2019-08-28 18:03
閱讀 2605·2019-08-26 12:22
閱讀 3437·2019-08-26 12:20
閱讀 1735·2019-08-26 11:56
閱讀 2257·2019-08-23 15:30