溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Node.js中事件循環(huán)、定時器和process.nextTick()的示例分析

發(fā)布時間:2021-09-03 13:31:13 來源:億速云 閱讀:105 作者:小新 欄目:web開發(fā)

這篇文章主要介紹Node.js中事件循環(huán)、定時器和process.nextTick()的示例分析,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!

什么是事件循環(huán)

盡管JavaScript是單線程的,但通過盡可能將操作放到系統(tǒng)內(nèi)核執(zhí)行,事件循環(huán)允許Node.js執(zhí)行非阻塞I/O操作。

由于現(xiàn)代大多數(shù)內(nèi)核都是多線程的,因此它們可以處理在后臺執(zhí)行的多個操作。 當(dāng)其中一個操作完成時,內(nèi)核會告訴Node.js,以便可以將相應(yīng)的回調(diào)添加到 輪詢隊列 中以最終執(zhí)行。 我們將在本主題后面進一步詳細解釋。

事件循環(huán)解釋

當(dāng)Node.js啟動時,它初始化事件循環(huán),處理提供的輸入腳本(或放入 REPL ,本文檔未涉及),這可能會進行異步API調(diào)用,調(diào)度計時器或調(diào)用 process.nextTick() , 然后開始處理事件循環(huán)。

下圖顯示了事件循環(huán)操作順序的簡要概述。

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

注意:每個框都將被稱為事件循環(huán)的“階段”。

每個階段都要執(zhí)行一個FIFO的回調(diào)隊列。 雖然每個階段都有其特殊的方式,但通常,當(dāng)事件循環(huán)進入給定階段時,它將執(zhí)行特定于該階段的任何操作,然后在該階段的隊列中執(zhí)行回調(diào),直到隊列耗盡或最大回調(diào)數(shù)量為止 。 當(dāng)隊列耗盡或達到回調(diào)限制時,事件循環(huán)將移至下一階段,依此類推。

由于這些操作中的任何一個可以調(diào)度更多操作并且在輪詢階段中處理的新事件由內(nèi)核排隊,因此輪詢事件可以在處理輪詢事件時排隊。 因此,長時間運行的回調(diào)可以允許輪詢階段運行的時間比計時器的閾值長得多。 有關(guān)詳細信息,請參閱和部分。

注意:Windows和Unix / Linux實現(xiàn)之間存在輕微差異,但這對于此演示并不重要。 最重要的部分在這里。 實際上有七到八個步驟,但我們關(guān)心的是 - Node.js實際使用的那些 - 是上面那些。

階段概述

  • timer : 此階段執(zhí)行 setTimeout() 和 setInterval() 調(diào)度的回調(diào)

  • pending callbacks : 執(zhí)行延遲到下一個循環(huán)迭代的I/O回調(diào)

  • idle, prepare : 只用于內(nèi)部

  • poll : 檢索新的I/O事件; 執(zhí)行與I/O相關(guān)的回調(diào)(幾乎所有回調(diào)都是帶有異常的 close callbacks , timers 和 setImmediate() 調(diào)度的回調(diào)); node將在適當(dāng)?shù)臅r候阻塞在這里

  • check : 這里調(diào)用 setImmediate() 回調(diào)函數(shù)

  • close callbacks : 一些 close callbacks, 例如. socket.on(‘close', …)

在事件循環(huán)的每次運行之間,Node.js檢查它是否在等待任何異步I / O或定時器,如果沒有,則關(guān)閉。

階段細節(jié)

定時器(timer)

計時器在一個回調(diào)執(zhí)行完之后指定閾值,而不是人們希望的確切時間去執(zhí)行。 定時器回調(diào)將在指定的時間過去后盡早安排; 但是,操作系統(tǒng)調(diào)度或其他回調(diào)的運行可能會延遲它們。

注意:從技術(shù)上講,控制何時執(zhí)行定時器。

例如,假設(shè)您計劃在100毫秒后執(zhí)行 timeout ,然后您的腳本將異步讀取一個耗時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)進入輪詢階段時,它有一個空隊列( fs.readFile() 尚未完成),因此它將等待剩余的ms數(shù),直到達到最快的計時器閾值。 當(dāng)它等待95毫秒傳遞時, fs.readFile() 完成讀取文件,并且其完成需要10毫秒的回調(diào)被添加到輪詢隊列并執(zhí)行。 當(dāng)回調(diào)結(jié)束時,隊列中不再有回調(diào),因此事件循環(huán)將看到已達到最快定時器的閾值,然后回繞到定時器階段以執(zhí)行定時器的回調(diào)。 在此示例中,您將看到正在調(diào)度的計時器與正在執(zhí)行的回調(diào)之間的總延遲將為105毫秒。

注意:為了防止輪詢階段使事件循環(huán)挨餓,libuv(實現(xiàn)Node.js事件循環(huán)的C庫和平臺的所有異步行為)在停止輪詢之前也為事件提供了固定的最大值(取決于系統(tǒng))。

等待回調(diào)(pending callbacks)

此階段執(zhí)行某些系統(tǒng)操作(例如TCP錯誤類型)的回調(diào)。 例如,如果TCP套接字在嘗試連接時收到 ECONNREFUSED ,則某些*nix系統(tǒng)希望等待報告錯誤。 這將排隊等待在等待回調(diào)階段執(zhí)行。

輪詢(poll)

輪詢階段有兩個主要功能:

1.計算它阻塞和輪詢I / O的時間,然后
2.處理輪詢隊列中的事件。

當(dāng)事件循環(huán)進入輪詢階段并且沒有定時器調(diào)度時,將發(fā)生以下兩種情況之一:

  • 如果輪詢隊列不為空,則事件循環(huán)將遍歷回調(diào)隊列并且同步執(zhí)行,直到隊列已執(zhí)行完或者達到系統(tǒng)相關(guān)的固定限制。

  • 如果輪詢隊列為空,則會發(fā)生以下兩種情況之一:

setImmediate()
setImmediate()

檢查(check)

此階段允許在輪詢階段完成后立即執(zhí)行回調(diào)。 如果輪詢階段變?yōu)榭臻e并且腳本已使用 setImmediate() 排隊,則事件循環(huán)可以繼續(xù)到檢查階段而不是等待。

setImmediate() 實際上是一個特殊的計時器,它在事件循環(huán)的一個單獨階段運行。 它使用libuv API來調(diào)度在輪詢階段完成后執(zhí)行的回調(diào)。

通常,在執(zhí)行代碼時,事件循環(huán)最終會到達輪詢階段,它將等待傳入連接,請求等。但是,如果已使用 setImmediate() 調(diào)度回調(diào)并且輪詢階段變?yōu)榭臻e,則 將結(jié)束并繼續(xù)檢查階段,而不是等待輪詢事件。

關(guān)閉回調(diào)(close callbacks)

如果套接字或句柄突然關(guān)閉( 例如socket.destroy() ),則在此階段將發(fā)出 'close' 事件。 否則它將通過 process.nextTick() 發(fā)出。

setImmediate() vs setTimeout()

setImmediate 和 setTimeout() 類似,但根據(jù)它們的調(diào)用時間以不同的方式運行。

setImmediate()
setTimeout()

執(zhí)行定時器的順序?qū)⒏鶕?jù)調(diào)用它們的上下文而有所不同。 如果從主模塊中調(diào)用兩者,則時間將受到進程性能的限制(可能受到計算機上運行的其他應(yīng)用程序的影響)。

例如,如果我們運行不在I / O周期內(nèi)的以下腳本(即主模塊),則執(zhí)行兩個定時器的順序是不確定的,因為它受進程性能的約束:

// 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周期內(nèi)移動兩個調(diào)用,則始終首先執(zhí)行立即回調(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)點是 setImmediate() 將始終在任何定時器之前執(zhí)行(如果在I / O周期內(nèi)調(diào)度),與存在多少定時器無關(guān)。

process.nextTick()

理解 process.nextTick()

您可能已經(jīng)注意到 process.nextTick() 沒有顯示在圖中,即使它是異步API的一部分。 這是因為 process.nextTick() 在技術(shù)上不是事件循環(huán)的一部分。 相反, nextTickQueue 將在當(dāng)前操作完成后處理,而不管事件循環(huán)的當(dāng)前階段如何。

回顧一下我們的圖表,無論何時在給定階段調(diào)用 process.nextTick() ,傳遞給 process.nextTick() 的所有回調(diào)都將在事件循環(huán)繼續(xù)之前得到解決。 這可能會產(chǎn)生一些不好的情況, 因為它允許您通過進行遞歸的 process.nextTick() 調(diào)用來“餓死”您的I / O, 這會阻止事件循環(huán)到達輪詢階段。

為什么會被允許?

為什么這樣的東西會被包含在Node.js中? 其中一部分是一種設(shè)計理念,其中API應(yīng)該始終是異步的,即使它不是必須的。 以此代碼段為例:

function apiCall(arg, callback) {
 if (typeof arg !== 'string')
 return process.nextTick(callback,
    new TypeError('argument should be string'));
}

這段代碼進行參數(shù)檢查,如果不正確,它會將錯誤傳遞給回調(diào)。 最近更新的API允許將參數(shù)傳遞給 process.nextTick() ,允許它將回調(diào)后傳遞的任何參數(shù)作為參數(shù)傳播到回調(diào),因此您不必嵌套函數(shù)。

我們正在做的是將錯誤傳回給用戶,但只有在我們允許其余的用戶代碼執(zhí)行之后。 通過使用 process.nextTick() ,我們保證 apiCall() 始終在用戶代碼的其余部分之后和允許事件循環(huán)繼續(xù)之前運行其回調(diào)。 為了實現(xiàn)這一點,允許JS調(diào)用堆棧展開然后立即執(zhí)行提供的回調(diào),這允許一個人對 process.nextTick() 進行遞歸調(diào)用而不會達到 RangeError :超出v8的最大調(diào)用堆棧大小。

這種理念可能會導(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() 定義為具有異步簽名,但它實際上是同步操作的。 調(diào)用它時,在事件循環(huán)的同一階段調(diào)用提供給 someAsyncApiCall() 的回調(diào),因為 someAsyncApiCall() 實際上不會異步執(zhí)行任何操作。 因此,回調(diào)嘗試引用bar,即使它在范圍內(nèi)可能沒有該變量,因為該腳本無法運行完成。

通過將回調(diào)放在 process.nextTick() 中,腳本仍然能夠運行完成,允許在調(diào)用回調(diào)之前初始化所有變量,函數(shù)等。 它還具有不允許事件循環(huán)繼續(xù)的優(yōu)點。 在允許事件循環(huán)繼續(xù)之前,向用戶警告錯誤可能是有用的。 以下是使用 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', () => {});

僅傳遞端口時,端口立即綁定。 因此,可以立即調(diào)用 'listening' 回調(diào)。 問題是那時候不會設(shè)置 .on('listening') 回調(diào)。

為了解決這個問題, 'listening' 事件在 nextTick() 中排隊,以允許腳本運行完成。 這允許用戶設(shè)置他們想要的任何事件處理程序。

process.nextTick() vs setImmediate()

就用戶而言,我們有兩個類似的調(diào)用,但它們的名稱令人困惑。

process.nextTick()
setImmediate()

實質(zhì)上,應(yīng)該交換名稱。 process.nextTick() 比 setImmediate() 更快地觸發(fā),但這是過去創(chuàng)造的,不太可能改變。 進行此切換會破壞npm上的大部分包。 每天都會添加更多新模塊,這意味著我們每天都在等待,更多的潛在破損發(fā)生。 雖然它們令人困惑,但自身的叫法不會改變。

我們建議開發(fā)人員在所有情況下都使用 setImmediate() ,因為它更容易推理(并且它導(dǎo)致代碼與更廣泛的環(huán)境兼容,如瀏覽器JS。)

為什么要使用 process.nextTick() ?

有兩個主要原因:

  • 允許用戶處理錯誤,清除任何不需要的資源,或者在事件循環(huán)繼續(xù)之前再次嘗試請求。

  • 有時需要允許回調(diào)在調(diào)用堆棧展開之后但在事件循環(huán)繼續(xù)之前運行。

一個例子是匹配用戶的期望。 簡單的例子:

const server = net.createServer();
server.on('connection', (conn) => { });
server.listen(8080);
server.on('listening', () => { });

假設(shè) listen() 在事件循環(huán)開始時運行,但是監(jiān)聽回調(diào)放在 setImmediate() 中。 除非傳遞主機名,否則將立即綁定到端口。 要使事件循環(huán)繼續(xù),它必須達到輪詢階段,這意味著可能已經(jīng)接收到連接的非零概率允許在偵聽事件之前觸發(fā)連接事件。

另一個例子是運行一個函數(shù)構(gòu)造函數(shù),比如繼承自 EventEmitter ,它想在構(gòu)造函數(shù)中調(diào)用一個事件:

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ā)出事件,因為腳本將不會處理到用戶為該事件分配回調(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!');
});

以上是“Node.js中事件循環(huán)、定時器和process.nextTick()的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對大家有幫助,更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI