摘要:回調函數執行幾乎所有的回調函數,除了關閉回調函數,定時器計劃的回調函數和。輪詢此階段有兩個主要的功能執行已過時的定時器腳本處理輪詢隊列中的事件。一旦輪詢隊列為空,事件循環將檢查已達到時間閾值的定時器。
什么是事件循環(Event Loop)?
事件環使得Node.js可以執行非阻塞I/O 操作,只要有可能就將操作卸載到系統內核,盡管JavaScript是單線程的。
由于大多數現代(終端)內核都是多線程的,他們可以處理在后臺執行的多個操作。 當其中一個操作完成時,內核會通知Node.js,以便可以將適當的回調添加到輪詢隊列poll queue中以最終執行。 我們將在本主題后面進一步詳細解釋這一點。
事件循環:解釋當Node.js開始運行,它初始化事件環、處理提供的輸入腳本(或放入REPL,本文檔未涉及),這可能會使異步API調用,計劃定時器或調用process.nextTick(),然后開始處理事件循環。
下圖顯示了事件循環的操作順序的簡化概述。
┌───────────────────────┐ ┌─>│ timers(計時器) │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare 內部 │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll(輪詢) │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘
注意:每個方框將被稱為事件循環的“階段”。
每個階段都有一個執行回調的FIFO(First In First Out,先進先出)隊列。 雖然每個階段都有其特定的方式,但通常情況下,當事件循環進入給定階段時,它將執行特定于該階段的任何操作,然后在該階段的隊列中執行回調,直到隊列耗盡或回調的最大數量已執行。 當隊列耗盡或達到回調限制時,事件循環將移至下一個階段,依此類推。
由于這些操作中的任何一個都可以調度更多的操作,并且在輪詢階段處理的新事件由內核排隊,所以輪詢事件可以在輪詢事件正在處理的同時排隊。 因此,長時間運行的回調可以使輪詢階段的運行時間遠遠超過計時器的閾值。有關更多詳細信息,請參閱定時器和輪詢部分。
注意:Windows和Unix / Linux實現之間略有差異,但這對此演示并不重要。 最重要的部分在這里。 實際上有七八個步驟,但我們關心的那些 - Node.js實際使用的那些 - 就是上述那些。階段概述
定時器(timers):此階段執行由setTimeout()和setInterval()調度的回調。
I / O 回調函數:執行幾乎所有的回調函數,除了關閉回調函數,定時器計劃的回調函數和setImmediate()。
閑置,準備(idle, prepare):只在Node內部使用。
輪詢(poll):檢索新的I / O事件; 適當時節點將在此處阻斷進程。
檢查(check):setImmediate()回調在這里被調用。
關閉回調(close callbacks):例如 socket.on("close",...)。
在事件循環的每次運行之間,Node.js檢查它是否正在等待任何異步I / O或定時器,并在沒有任何異步I / O或定時器時清除關閉。
階段詳情定時器
計時器指定閾值,之后可以執行提供的回調,而不是人們希望執行的確切時間。 定時器回調將在指定的時間過后,按照預定的時間運行; 但是,操作系統調度或其他回調的運行可能會延遲它們。
注意:從技術上講,輪詢階段控制何時執行定時器。
例如,假設您計劃在100 ms閾值后執行超時,那么您的腳本將異步開始讀取需要95 ms的文件:
const fs = require("fs"); function someAsyncOperation(callback) { // 假設這個讀取將用耗時95ms 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); // 執行一些異步操作將耗時 95ms someAsyncOperation(() => { const startCallback = Date.now(); // 執行一些可能耗時10ms的操作 while (Date.now() - startCallback < 10) { // do nothing } });
當事件循環進入輪詢階段時,它有一個空隊列(fs.readFile()尚未完成),因此它將等待剩余的毫秒數,直到達到最快計時器的閾值。 當它等待95ms傳遞時,fs.readFile()完成讀取文件,并且需要10ms完成的回調被添加到輪詢隊列并執行。 當回調完成時,隊列中沒有更多的回調,所以事件循環會看到已經達到最快計時器的閾值,然后回到計時器階段以執行計時器的回調。 在這個例子中,你會看到被調度的定時器和它正在執行的回調之間的總延遲將是105ms。
注意:為防止輪詢階段進入惡性事件循環,在停止輪詢之前,libuv(實現Node.js事件循環的C庫以及平臺的所有異步行為)也有一個硬性最大值(取決于系統)來停止輪詢更多的事件。
I / O回調
此階段為某些系統操作(如TCP錯誤類型)執行回調。 例如,如果嘗試連接時TCP套接字收到ECONNREFUSED,則某些* nix系統要等待報告錯誤。 這將排隊在I / O回調階段執行。
輪詢
此階段有兩個主要的功能:
執行已過時的定時器腳本;
處理輪詢隊列中的事件。
當事件循環進入輪詢階段并且沒有計時器時,會發生以下兩件事之一:
如果輪詢隊列不為空,則事件循環將遍歷其回調隊列,同步執行它們,直到隊列耗盡或達到系統相關硬限制。
如果輪詢隊列為空,則會發生以下兩件事之一:
1)如果腳本已通過setImmediate()進行調度,則事件循環將結束輪詢階段并繼續執行檢查階段以執行這些預定腳本。
2)如果腳本沒有通過setImmediate()進行調度,則事件循環將等待回調被添加到隊列中,然后立即執行它們。
一旦輪詢隊列為空,事件循環將檢查已達到時間閾值的定時器。 如果一個或多個定時器準備就緒,則事件循環將回退到定時器階段以執行這些定時器的回調。
檢查check
此階段允許在輪詢階段結束后立即執行回調。 如果輪詢階段變得空閑并且腳本已經使用setImmediate()排隊,則事件循環可能會繼續檢查階段而不是等待。
setImmediate()實際上是一個特殊的定時器,它在事件循環的一個多帶帶的階段中運行。 它使用libuv API來調度回調,以在輪詢階段完成后執行。
通常,隨著代碼的執行,事件循環將最終進入輪詢階段,在那里它將等待傳入的連接,請求等。但是,如果使用setImmediate()計劃了回調并且輪詢階段變為空閑, 將結束并繼續進行檢查階段,而不是等待輪詢事件。
關閉回調
如果套接字或句柄突然關閉(例如socket.destroy()),則在此階段將發出"close"事件。 否則它將通過process.nextTick()發出。
setImmediate()vs setTimeout()
setImmediate()和setTimeout()是相似的,但取決于它們何時被調用,其行為方式不同。
setImmediate()用于在當前輪詢階段完成后執行腳本。
setTimeout()計劃腳本在經過最小閾值(以毫秒為單位)后運行。
定時器執行的順序取決于它們被調用的上下文。 如果兩者都是在主模塊內調用的,那么時序將受到進程性能的限制(可能會受到計算機上運行的其他應用程序的影響)。
例如,如果我們運行以下不在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周期內移動這兩個調用,則立即回調總是首先執行:
// 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()
您可能已經注意到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:超出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()實際上并不會異步執行任何操作。 因此,回調會嘗試引用欄,即使它在范圍中可能沒有該變量,因為該腳本無法運行到完成狀態。
通過將回調放置在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()觸發以下迭代或事件循環的“打勾”
實質上,名稱應該交換。 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); // 一旦處理程序被分配,使用nextTick來發出事件 process.nextTick(() => { this.emit("event"); }); } util.inherits(MyEmitter, EventEmitter); const myEmitter = new MyEmitter(); myEmitter.on("event", () => { console.log("an event occurred!"); });
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/107645.html
js異步歷史 一個 JavaScript 引擎會常駐于內存中,它等待著我們把JavaScript 代碼或者函數傳遞給它執行 在 ES3 和更早的版本中,JavaScript 本身還沒有異步執行代碼的能力,引擎就把代碼直接順次執行了,異步任務都是宿主環境(瀏覽器)發起的(setTimeout、AJAX等)。 在 ES5 之后,JavaScript 引入了 Promise,這樣,不需要瀏覽器的安排,J...
摘要:前沿是基于引擎的運行環境具有事件驅動非阻塞等特點結合具有網絡編程文件系統等服務端的功能用庫進行異步事件處理線程的單線程含義實際上說的是執行同步代碼的主線程一個程序的啟動不止是分配了一個線程,而是我們只能在一個線程執行代碼當出現資源調用連接等 前沿 Node.js 是基于V8引擎的javascript運行環境. Node.js具有事件驅動, 非阻塞I/O等特點. 結合Node API, ...
摘要:事件觸發線程主要負責將準備好的事件交給引擎線程執行。它將不同的任務分配給不同的線程,形成一個事件循環,以異步的方式將任務的執行結果返回給引擎。 Fundebug經作者浪里行舟授權首發,未經同意請勿轉載。 前言 本文我們將會介紹 JS 實現異步的原理,并且了解了在瀏覽器和 Node 中 Event Loop 其實是不相同的。 一、線程與進程 1. 概念 我們經常說 JS 是單線程執行的,...
摘要:階段有兩個主要功能也會執行時間定時器到達期望時間的回調函數執行事件循環列表里的函數當進入階段并且沒有其余的定時器,那么如果事件循環列表不為空,則迭代同步的執行隊列中的函數。如果沒有,則等待回調函數進入隊列并立即執行。 Event Loop 本文以 Node.js 為例,講解 Event Loop 在 Node.js 的實現,原文,JavaScript 中的實現大同小異。 什么是 Eve...
摘要:通過查看的文檔可以發現整個分為個階段定時器相關任務,中我們關注的是它會執行和中到期的回調執行某些系統操作的回調內部使用執行,一定條件下會在這個階段阻塞住執行的回調如果或者關閉了,就會在這個階段觸發事件,執行事件的回調的代碼在文件中。 showImg(https://segmentfault.com/img/bVbd7B7?w=1227&h=644); 這次我們就不要那么多前戲,直奔主題...
閱讀 2737·2021-11-22 15:22
閱讀 1649·2021-11-22 14:56
閱讀 3626·2021-09-22 15:12
閱讀 2414·2021-09-02 15:41
閱讀 2133·2021-08-27 16:26
閱讀 1122·2019-08-30 15:55
閱讀 2150·2019-08-29 17:30
閱讀 675·2019-08-29 16:26