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í)行耿戚。 我們將在本主題后面進(jìn)一步詳細(xì)解釋。

事件循環(huán)解釋

當(dāng)Node.js啟動時坛猪,它初始化事件循環(huán),處理提供的輸入腳本(或放入REPL就斤,本文檔未涉及)洋机,這可能會進(jìn)行異步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)進(jìn)入給定階段時不瓶,它將執(zhí)行特定于該階段的任何操作禾嫉,然后在該階段的隊列中執(zhí)行回調(diào),直到隊列耗盡或最大回調(diào)數(shù)量為止 蚊丐。 當(dāng)隊列耗盡或達(dá)到回調(diào)限制時熙参,事件循環(huán)將移至下一階段,依此類推麦备。

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

注意: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 callbackstimerssetImmediate()調(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)閉。

階段細(xì)節(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)進(jìn)入輪詢階段時逸月,它有一個空隊列(fs.readFile()尚未完成)肛响,因此它將等待剩余的ms數(shù)猎物,直到達(dá)到最快的計時器閾值。 當(dāng)它等待95毫秒傳遞時搀罢,fs.readFile()完成讀取文件铅鲤,并且其完成需要10毫秒的回調(diào)被添加到輪詢隊列并執(zhí)行。 當(dāng)回調(diào)結(jié)束時,隊列中不再有回調(diào),因此事件循環(huán)將看到已達(dá)到最快定時器的閾值溢陪,然后回繞到定時器階段以執(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)進(jìn)入輪詢階段并且沒有定時器調(diào)度時粹湃,將發(fā)生以下兩種情況之一:

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

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

    1. 如果setImmediate()已調(diào)度腳本纵搁,則事件循環(huán)將結(jié)束輪詢階段并繼續(xù)執(zhí)行檢查階段以執(zhí)行這些調(diào)度腳本跷敬。

    2. 如果setImmediate()尚未調(diào)度腳本饺鹃,則事件循環(huán)將等待將回調(diào)添加到隊列,然后立即執(zhí)行它們。

檢查(check)

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

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

通常枢贿,在執(zhí)行代碼時殉农,事件循環(huán)最終會到達(dá)輪詢階段,它將等待傳入連接局荚,請求等超凳。但是,如果已使用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()

setImmediatesetTimeout()類似簿盅,但根據(jù)它們的調(diào)用時間以不同的方式運行挥下。

  • setImmediate()用于在當(dāng)前輪詢階段完成后執(zhí)行腳本。
  • setTimeout()計劃在經(jīng)過最小閾值(以ms為單位)后運行的腳本桨醋。

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

例如偎蘸,如果我們運行不在I / O周期內(nèi)的以下腳本(即主模塊),則執(zhí)行兩個定時器的順序是不確定的瞬内,因為它受進(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

但是迷雪,如果在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)生一些不好的情況,因為它允許您通過進(jìn)行遞歸的process.nextTick()調(diào)用來“餓死”您的I / O踱承,這會阻止事件循環(huán)到達(dá)輪詢階段倡缠。

為什么會被允許?

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

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

這段代碼進(jìn)行參數(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()進(jìn)行遞歸調(diào)用而不會達(dá)到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()在同一階段立即觸發(fā)
  • setImmediate()在事件循環(huán)的后續(xù)迭代或'tick'中觸發(fā)

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

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

為什么要使用process.nextTick()

有兩個主要原因:

  1. 允許用戶處理錯誤棠耕,清除任何不需要的資源余佛,或者在事件循環(huán)繼續(xù)之前再次嘗試請求。
  2. 有時需要允許回調(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()中蕊退。 除非傳遞主機(jī)名郊楣,否則將立即綁定到端口。 要使事件循環(huán)繼續(xù)瓤荔,它必須達(dá)到輪詢階段净蚤,這意味著可能已經(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!');
});
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末哥童,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子褒翰,更是在濱河造成了極大的恐慌如蚜,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件影暴,死亡現(xiàn)場離奇詭異错邦,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)型宙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進(jìn)店門撬呢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人妆兑,你說我怎么就攤上這事魂拦∶牵” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵芯勘,是天一觀的道長箱靴。 經(jīng)常有香客問我,道長荷愕,這世上最難降的妖魔是什么衡怀? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮安疗,結(jié)果婚禮上抛杨,老公的妹妹穿的比我還像新娘。我一直安慰自己荐类,他們只是感情好怖现,可當(dāng)我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著玉罐,像睡著了一般屈嗤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吊输,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天恢共,我揣著相機(jī)與錄音,去河邊找鬼璧亚。 笑死讨韭,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的癣蟋。 我是一名探鬼主播透硝,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼疯搅!你這毒婦竟也來了濒生?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤幔欧,失蹤者是張志新(化名)和其女友劉穎罪治,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體礁蔗,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡觉义,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了浴井。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晒骇。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出洪囤,到底是詐尸還是另有隱情徒坡,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布瘤缩,位于F島的核電站喇完,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏剥啤。R本人自食惡果不足惜锦溪,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望铐殃。 院中可真熱鬧,春花似錦跨新、人聲如沸富腊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赘被。三九已至,卻和暖如春肖揣,著一層夾襖步出監(jiān)牢的瞬間民假,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工龙优, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留羊异,地道東北人。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓彤断,卻偏偏與公主長得像野舶,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子宰衙,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,781評論 2 354