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