node.js的事件循環(huán)

在node中恒削,事件循環(huán)表現(xiàn)出的狀態(tài)與瀏覽器中大致相同。不同的是node中有一套自己的模型。node中事件循環(huán)的實(shí)現(xiàn)是依靠的libuv引擎。我們知道node選擇chrome v8引擎作為js解釋器吊骤,v8引擎將js代碼分析后去調(diào)用對(duì)應(yīng)的node api,而這些api最后則由libuv引擎驅(qū)動(dòng)静尼,執(zhí)行對(duì)應(yīng)的任務(wù)白粉,并把不同的事件放在不同的隊(duì)列中等待主線程執(zhí)行传泊。 因此實(shí)際上node中的事件循環(huán)存在于libuv引擎中。

為了協(xié)調(diào)異步任務(wù)鸭巴,Node 居然提供了四個(gè)定時(shí)器眷细,讓任務(wù)可以在指定的時(shí)間運(yùn)行。

setTimeout()
setInterval()
setImmediate()
process.nextTick()
前兩個(gè)是語(yǔ)言的標(biāo)準(zhǔn)鹃祖,后兩個(gè)是 Node 獨(dú)有的溪椎。它們的寫法差不多,作用也差不多恬口,不太容易區(qū)別池磁。

你能說(shuō)出下面代碼的運(yùn)行結(jié)果嗎?

// test.js
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
(() => console.log(5))();
運(yùn)行結(jié)果如下楷兽。

$ node test.js
5
3
4
1
2

一地熄、同步任務(wù)和異步任務(wù)

首先,同步任務(wù)總是比異步任務(wù)更早執(zhí)行芯杀。

前面的那段代碼端考,只有最后一行是同步任務(wù),因此最早執(zhí)行揭厚。


(() => console.log(5))();

二却特、本輪循環(huán)和次輪循環(huán)

異步任務(wù)可以分成兩種。

  • 追加在本輪循環(huán)的異步任務(wù)
  • 追加在次輪循環(huán)的異步任務(wù)

所謂"循環(huán)"筛圆,指的是事件循環(huán)(event loop)裂明。這是 JavaScript 引擎處理異步任務(wù)的方式,后文會(huì)詳細(xì)解釋太援。這里只要理解闽晦,本輪循環(huán)一定早于次輪循環(huán)執(zhí)行即可。

Node 規(guī)定提岔,process.nextTickPromise的回調(diào)函數(shù)仙蛉,追加在本輪循環(huán),即同步任務(wù)一旦執(zhí)行完成碱蒙,就開始執(zhí)行它們荠瘪。而setTimeoutsetInterval赛惩、setImmediate的回調(diào)函數(shù)哀墓,追加在次輪循環(huán)。

這就是說(shuō)喷兼,文首那段代碼的第三行和第四行篮绰,一定比第一行和第二行更早執(zhí)行。


// 下面兩行褒搔,次輪循環(huán)執(zhí)行
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
// 下面兩行阶牍,本輪循環(huán)執(zhí)行
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));

三、process.nextTick()

process.nextTick這個(gè)名字有點(diǎn)誤導(dǎo)星瘾,它是在本輪循環(huán)執(zhí)行的走孽,而且是所有異步任務(wù)里面最快執(zhí)行的。

image

Node 執(zhí)行完所有同步任務(wù)琳状,接下來(lái)就會(huì)執(zhí)行process.nextTick的任務(wù)隊(duì)列磕瓷。所以,下面這行代碼是第二個(gè)輸出結(jié)果念逞。


process.nextTick(() => console.log(3));

基本上困食,如果你希望異步任務(wù)盡可能快地執(zhí)行,那就使用process.nextTick翎承。

四硕盹、微任務(wù)

根據(jù)語(yǔ)言規(guī)格,Promise對(duì)象的回調(diào)函數(shù)叨咖,會(huì)進(jìn)入異步任務(wù)里面的"微任務(wù)"(microtask)隊(duì)列瘩例。

微任務(wù)隊(duì)列追加在process.nextTick隊(duì)列的后面,也屬于本輪循環(huán)甸各。所以垛贤,下面的代碼總是先輸出3,再輸出4趣倾。


process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
// 3
// 4

image

注意聘惦,只有前一個(gè)隊(duì)列全部清空以后,才會(huì)執(zhí)行下一個(gè)隊(duì)列儒恋。


process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
// 1
// 3
// 2
// 4

上面代碼中善绎,全部process.nextTick的回調(diào)函數(shù),執(zhí)行都會(huì)早于Promise的诫尽。

至此涂邀,本輪循環(huán)的執(zhí)行順序就講完了。

  1. 同步任務(wù)
  2. process.nextTick()
  3. 微任務(wù)

五箱锐、事件循環(huán)的概念

下面開始介紹次輪循環(huán)的執(zhí)行順序比勉,這就必須理解什么是事件循環(huán)(event loop)了。

Node 的官方文檔是這樣介紹的驹止。

"When Node.js starts, it initializes the event loop, processes the provided input script which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop."

這段話很重要浩聋,需要仔細(xì)讀。它表達(dá)了三層意思臊恋。

首先衣洁,有些人以為,除了主線程抖仅,還存在一個(gè)單獨(dú)的事件循環(huán)線程坊夫。不是這樣的砖第,只有一個(gè)主線程,事件循環(huán)是在主線程上完成的环凿。

其次梧兼,Node 開始執(zhí)行腳本時(shí),會(huì)先進(jìn)行事件循環(huán)的初始化智听,但是這時(shí)事件循環(huán)還沒(méi)有開始羽杰,會(huì)先完成下面的事情。

  • 同步任務(wù)
  • 發(fā)出異步請(qǐng)求
  • 規(guī)劃定時(shí)器生效的時(shí)間
  • 執(zhí)行process.nextTick()等等

最后到推,上面這些事情都干完了考赛,事件循環(huán)就正式開始了。

六莉测、事件循環(huán)的六個(gè)階段

事件循環(huán)會(huì)無(wú)限次地執(zhí)行颜骤,一輪又一輪。只有異步任務(wù)的回調(diào)函數(shù)隊(duì)列清空了捣卤,才會(huì)停止執(zhí)行复哆。

每一輪的事件循環(huán),分成六個(gè)階段腌零。這些階段會(huì)依次執(zhí)行梯找。

  1. timers
  2. I/O callbacks
  3. idle, prepare
  4. poll
  5. check
  6. close callbacks

每個(gè)階段都有一個(gè)先進(jìn)先出的回調(diào)函數(shù)隊(duì)列。只有一個(gè)階段的回調(diào)函數(shù)隊(duì)列清空了益涧,該執(zhí)行的回調(diào)函數(shù)都執(zhí)行了锈锤,事件循環(huán)才會(huì)進(jìn)入下一個(gè)階段。

image

下面簡(jiǎn)單介紹一下每個(gè)階段的含義闲询,詳細(xì)介紹可以看官方文檔久免,也可以參考 libuv 的源碼解讀

(1)timers

這個(gè)是定時(shí)器階段扭弧,處理setTimeout()setInterval()的回調(diào)函數(shù)阎姥。進(jìn)入這個(gè)階段后,主線程會(huì)檢查一下當(dāng)前時(shí)間鸽捻,是否滿足定時(shí)器的條件呼巴。如果滿足就執(zhí)行回調(diào)函數(shù),否則就離開這個(gè)階段御蒲。

(2)I/O callbacks

除了以下操作的回調(diào)函數(shù)衣赶,其他的回調(diào)函數(shù)都在這個(gè)階段執(zhí)行。

  • setTimeout()setInterval()的回調(diào)函數(shù)
  • setImmediate()的回調(diào)函數(shù)
  • 用于關(guān)閉請(qǐng)求的回調(diào)函數(shù)厚满,比如socket.on('close', ...)

(3)idle, prepare

該階段只供 libuv 內(nèi)部調(diào)用,這里可以忽略鲸郊。

(4)Poll

這個(gè)階段是輪詢時(shí)間秆撮,用于等待還未返回的 I/O 事件峻黍,比如服務(wù)器的回應(yīng)姆涩、用戶移動(dòng)鼠標(biāo)等等骨饿。

這個(gè)階段的時(shí)間會(huì)比較長(zhǎng)宏赘。如果沒(méi)有其他異步任務(wù)要處理(比如到期的定時(shí)器)察署,會(huì)一直停留在這個(gè)階段脐往,等待 I/O 請(qǐng)求返回結(jié)果业簿。

(5)check

該階段執(zhí)行setImmediate()的回調(diào)函數(shù)。

(6)close callbacks

該階段執(zhí)行關(guān)閉請(qǐng)求的回調(diào)函數(shù)克饶,比如socket.on('close', ...)

七邀跃、事件循環(huán)的示例

下面是來(lái)自官方文檔的一個(gè)示例途戒。


const fs = require('fs');

const timeoutScheduled = Date.now();

// 異步任務(wù)一:100ms 后執(zhí)行的定時(shí)器
setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;
  console.log(`${delay}ms`);
}, 100);

// 異步任務(wù)二:文件讀取后蒜茴,有一個(gè) 200ms 的回調(diào)函數(shù)
fs.readFile('test.js', () => {
  const startCallback = Date.now();
  while (Date.now() - startCallback < 200) {
    // 什么也不做
  }
});

上面代碼有兩個(gè)異步任務(wù)粉私,一個(gè)是 100ms 后執(zhí)行的定時(shí)器,一個(gè)是文件讀取诺核,它的回調(diào)函數(shù)需要 200ms抄肖。請(qǐng)問(wèn)運(yùn)行結(jié)果是什么?

image

腳本進(jìn)入第一輪事件循環(huán)以后窖杀,沒(méi)有到期的定時(shí)器漓摩,也沒(méi)有已經(jīng)可以執(zhí)行的 I/O 回調(diào)函數(shù),所以會(huì)進(jìn)入 Poll 階段入客,等待內(nèi)核返回文件讀取的結(jié)果幌甘。由于讀取小文件一般不會(huì)超過(guò) 100ms,所以在定時(shí)器到期之前痊项,Poll 階段就會(huì)得到結(jié)果锅风,因此就會(huì)繼續(xù)往下執(zhí)行。

第二輪事件循環(huán)鞍泉,依然沒(méi)有到期的定時(shí)器皱埠,但是已經(jīng)有了可以執(zhí)行的 I/O 回調(diào)函數(shù),所以會(huì)進(jìn)入 I/O callbacks 階段咖驮,執(zhí)行fs.readFile的回調(diào)函數(shù)边器。這個(gè)回調(diào)函數(shù)需要 200ms,也就是說(shuō)托修,在它執(zhí)行到一半的時(shí)候忘巧,100ms 的定時(shí)器就會(huì)到期。但是睦刃,必須等到這個(gè)回調(diào)函數(shù)執(zhí)行完砚嘴,才會(huì)離開這個(gè)階段。

第三輪事件循環(huán),已經(jīng)有了到期的定時(shí)器际长,所以會(huì)在 timers 階段執(zhí)行定時(shí)器耸采。最后輸出結(jié)果大概是200多毫秒。

八工育、setTimeout 和 setImmediate

由于setTimeout在 timers 階段執(zhí)行虾宇,而setImmediate在 check 階段執(zhí)行。所以如绸,setTimeout會(huì)早于setImmediate完成嘱朽。


setTimeout(() => console.log(1));
setImmediate(() => console.log(2));

上面代碼應(yīng)該先輸出1,再輸出2怔接,但是實(shí)際執(zhí)行的時(shí)候搪泳,結(jié)果卻是不確定,有時(shí)還會(huì)先輸出2蜕提,再輸出1森书。

這是因?yàn)?code>setTimeout的第二個(gè)參數(shù)默認(rèn)為0靶端。但是實(shí)際上谎势,Node 做不到0毫秒,最少也需要1毫秒杨名,根據(jù)官方文檔脏榆,第二個(gè)參數(shù)的取值范圍在1毫秒到2147483647毫秒之間。也就是說(shuō)台谍,setTimeout(f, 0)等同于setTimeout(f, 1)须喂。

實(shí)際執(zhí)行的時(shí)候,進(jìn)入事件循環(huán)以后趁蕊,有可能到了1毫秒坞生,也可能還沒(méi)到1毫秒,取決于系統(tǒng)當(dāng)時(shí)的狀況掷伙。如果沒(méi)到1毫秒是己,那么 timers 階段就會(huì)跳過(guò),進(jìn)入 check 階段任柜,先執(zhí)行setImmediate的回調(diào)函數(shù)卒废。

但是,下面的代碼一定是先輸出2宙地,再輸出1摔认。


const fs = require('fs');

fs.readFile('test.js', () => {
  setTimeout(() => console.log(1));
  setImmediate(() => console.log(2));
});

上面代碼會(huì)先進(jìn)入 I/O callbacks 階段,然后是 check 階段宅粥,最后才是 timers 階段参袱。因此,setImmediate才會(huì)早于setTimeout執(zhí)行。

參考鏈接:
http://www.ruanyifeng.com/blog/2018/02/node-event-loop.html
https://zhuanlan.zhihu.com/p/38395184
https://segmentfault.com/a/1190000012258592

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蓖柔,一起剝皮案震驚了整個(gè)濱河市辰企,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌况鸣,老刑警劉巖牢贸,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異镐捧,居然都是意外死亡潜索,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門懂酱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)竹习,“玉大人,你說(shuō)我怎么就攤上這事列牺≌埃” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵瞎领,是天一觀的道長(zhǎng)泌辫。 經(jīng)常有香客問(wèn)我,道長(zhǎng)九默,這世上最難降的妖魔是什么震放? 我笑而不...
    開封第一講書人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮驼修,結(jié)果婚禮上殿遂,老公的妹妹穿的比我還像新娘。我一直安慰自己乙各,他們只是感情好墨礁,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著耳峦,像睡著了一般恩静。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上妇萄,一...
    開封第一講書人閱讀 49,829評(píng)論 1 290
  • 那天蜕企,我揣著相機(jī)與錄音,去河邊找鬼冠句。 笑死轻掩,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的懦底。 我是一名探鬼主播唇牧,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼罕扎,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了丐重?” 一聲冷哼從身側(cè)響起腔召,我...
    開封第一講書人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎扮惦,沒(méi)想到半個(gè)月后臀蛛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡崖蜜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年浊仆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片豫领。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡抡柿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出等恐,到底是詐尸還是另有隱情洲劣,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布课蔬,位于F島的核電站囱稽,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏购笆。R本人自食惡果不足惜粗悯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一虚循、第九天 我趴在偏房一處隱蔽的房頂上張望同欠。 院中可真熱鬧,春花似錦横缔、人聲如沸铺遂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)襟锐。三九已至,卻和暖如春膛锭,著一層夾襖步出監(jiān)牢的瞬間粮坞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工初狰, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留莫杈,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓奢入,卻偏偏與公主長(zhǎng)得像筝闹,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容