一次弄懂Event Loop(徹底解決此類面試問(wèn)題) 2021-03-22

前言

Event Loop即事件循環(huán)垂涯,是指瀏覽器或Node的一種解決javaScript單線程運(yùn)行時(shí)不會(huì)阻塞的一種機(jī)制嵌巷,也就是我們經(jīng)常使用異步的原理重窟。

為啥要弄懂Event Loop

  • 是要增加自己技術(shù)的深度鸟雏,也就是懂得JavaScript的運(yùn)行機(jī)制享郊。

  • 現(xiàn)在在前端領(lǐng)域各種技術(shù)層出不窮,掌握底層原理孝鹊,可以讓自己以不變炊琉,應(yīng)萬(wàn)變。

  • 應(yīng)對(duì)各大互聯(lián)網(wǎng)公司的面試又活,懂其原理苔咪,題目任其發(fā)揮。

堆柳骄,棧悼泌、隊(duì)列

[圖片上傳中...(image-ea01c-1616378768948-12)]

<figcaption></figcaption>

堆(Heap)

是一種數(shù)據(jù)結(jié)構(gòu),是利用完全二叉樹(shù)維護(hù)的一組數(shù)據(jù)夹界,分為兩種馆里,一種為最大,一種為最小堆可柿,將根節(jié)點(diǎn)最大叫做最大堆大根堆鸠踪,根節(jié)點(diǎn)最小叫做最小堆小根堆
線性數(shù)據(jù)結(jié)構(gòu)复斥,相當(dāng)于一維數(shù)組营密,有唯一后繼。

如最大堆

[圖片上傳中...(image-799453-1616378768948-11)]

<figcaption></figcaption>

棧(Stack)

在計(jì)算機(jī)科學(xué)中是限定僅在表尾進(jìn)行插入刪除操作的線性表目锭。 是一種數(shù)據(jù)結(jié)構(gòu)评汰,它按照后進(jìn)先出的原則存儲(chǔ)數(shù)據(jù),先進(jìn)入的數(shù)據(jù)被壓入棧底痢虹,最后的數(shù)據(jù)棧頂被去,需要讀數(shù)據(jù)的時(shí)候從棧頂開(kāi)始彈出數(shù)據(jù)
是只能在某一端插入刪除特殊線性表奖唯。

[圖片上傳中...(image-55a07b-1616378768948-10)]

<figcaption></figcaption>

隊(duì)列(Queue)

特殊之處在于它只允許在表的前端(front)進(jìn)行刪除操作惨缆,而在表的后端(rear)進(jìn)行插入操作,和一樣丰捷,隊(duì)列是一種操作受限制的線性表坯墨。
進(jìn)行插入操作的端稱為隊(duì)尾,進(jìn)行刪除操作的端稱為隊(duì)頭病往。 隊(duì)列中沒(méi)有元素時(shí)捣染,稱為空隊(duì)列

隊(duì)列的數(shù)據(jù)元素又稱為隊(duì)列元素停巷。在隊(duì)列中插入一個(gè)隊(duì)列元素稱為入隊(duì)耍攘,從隊(duì)列刪除一個(gè)隊(duì)列元素稱為出隊(duì)榕栏。因?yàn)殛?duì)列只允許在一端插入,在另一端刪除少漆,所以只有最早進(jìn)入隊(duì)列的元素才能最先從隊(duì)列中刪除臼膏,故隊(duì)列又稱為先進(jìn)先出FIFO—first in first out

[圖片上傳中...(image-c4c0d6-1616378768947-9)]

<figcaption></figcaption>

Event Loop

JavaScript中,任務(wù)被分為兩種示损,一種宏任務(wù)(MacroTask)也叫Task渗磅,一種叫微任務(wù)(MicroTask)。

MacroTask(宏任務(wù))

  • script全部代碼检访、setTimeout始鱼、setIntervalsetImmediate(瀏覽器暫時(shí)不支持脆贵,只有IE10支持医清,具體可見(jiàn)MDN)、I/O卖氨、UI Rendering会烙。

MicroTask(微任務(wù))

  • Process.nextTick(Node獨(dú)有)Promise筒捺、Object.observe(廢棄)柏腻、MutationObserver(具體使用方式查看這里

瀏覽器中的Event Loop

Javascript 有一個(gè) main thread 主線程和 call-stack 調(diào)用棧(執(zhí)行棧),所有的任務(wù)都會(huì)被放到調(diào)用棧等待主線程執(zhí)行系吭。

JS調(diào)用棧

JS調(diào)用棧采用的是后進(jìn)先出的規(guī)則五嫂,當(dāng)函數(shù)執(zhí)行的時(shí)候,會(huì)被添加到棧的頂部肯尺,當(dāng)執(zhí)行棧執(zhí)行完成后沃缘,就會(huì)從棧頂移出,直到棧內(nèi)被清空则吟。

同步任務(wù)和異步任務(wù)

Javascript單線程任務(wù)被分為同步任務(wù)異步任務(wù)槐臀,同步任務(wù)會(huì)在調(diào)用棧中按照順序等待主線程依次執(zhí)行,異步任務(wù)會(huì)在異步任務(wù)有了結(jié)果后逾滥,將注冊(cè)的回調(diào)函數(shù)放入任務(wù)隊(duì)列中等待主線程空閑的時(shí)候(調(diào)用棧被清空)峰档,被讀取到棧內(nèi)等待主線程的執(zhí)行。

[圖片上傳中...(image-2b02ac-1616378768947-8)]

<figcaption></figcaption>

任務(wù)隊(duì)列Task Queue寨昙,即隊(duì)列,是一種先進(jìn)先出的一種數(shù)據(jù)結(jié)構(gòu)掀亩。[圖片上傳中...(image-8552e3-1616378768947-7)]

<figcaption></figcaption>

事件循環(huán)的進(jìn)程模型

  • 選擇當(dāng)前要執(zhí)行的任務(wù)隊(duì)列舔哪,選擇任務(wù)隊(duì)列中最先進(jìn)入的任務(wù),如果任務(wù)隊(duì)列為空即null槽棍,則執(zhí)行跳轉(zhuǎn)到微任務(wù)(MicroTask)的執(zhí)行步驟捉蚤。
  • 將事件循環(huán)中的任務(wù)設(shè)置為已選擇任務(wù)抬驴。
  • 執(zhí)行任務(wù)。
  • 將事件循環(huán)中當(dāng)前運(yùn)行任務(wù)設(shè)置為null缆巧。
  • 將已經(jīng)運(yùn)行完成的任務(wù)從任務(wù)隊(duì)列中刪除布持。
  • microtasks步驟:進(jìn)入microtask檢查點(diǎn)。
  • 更新界面渲染陕悬。
  • 返回第一步题暖。

執(zhí)行進(jìn)入microtask檢查點(diǎn)時(shí),用戶代理會(huì)執(zhí)行以下步驟:

  • 設(shè)置microtask檢查點(diǎn)標(biāo)志為true捉超。
  • 當(dāng)事件循環(huán)microtask執(zhí)行不為空時(shí):選擇一個(gè)最先進(jìn)入的microtask隊(duì)列的microtask胧卤,將事件循環(huán)的microtask設(shè)置為已選擇的microtask,運(yùn)行microtask拼岳,將已經(jīng)執(zhí)行完成的microtasknull枝誊,移出microtask中的microtask
  • 清理IndexDB事務(wù)
  • 設(shè)置進(jìn)入microtask檢查點(diǎn)的標(biāo)志為false惜纸。

上述可能不太好理解叶撒,下圖是我做的一張圖片。

[圖片上傳中...(image-99eb39-1616378768947-6)]

<figcaption></figcaption>

執(zhí)行棧在執(zhí)行完同步任務(wù)后耐版,查看執(zhí)行棧是否為空祠够,如果執(zhí)行棧為空,就會(huì)去檢查微任務(wù)(microTask)隊(duì)列是否為空椭更,如果為空的話哪审,就執(zhí)行Task(宏任務(wù)),否則就一次性執(zhí)行完所有微任務(wù)虑瀑。
每次單個(gè)宏任務(wù)執(zhí)行完畢后湿滓,檢查微任務(wù)(microTask)隊(duì)列是否為空,如果不為空的話舌狗,會(huì)按照先入先出的規(guī)則全部執(zhí)行完微任務(wù)(microTask)后叽奥,設(shè)置微任務(wù)(microTask)隊(duì)列為null,然后再執(zhí)行宏任務(wù)痛侍,如此循環(huán)朝氓。

舉個(gè)例子

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});
console.log('script end');
復(fù)制代碼

首先我們劃分幾個(gè)分類:

第一次執(zhí)行:

Tasks:run script、 setTimeout callback

Microtasks:Promise then 

JS stack: script    
Log: script start主届、script end赵哲。
復(fù)制代碼

執(zhí)行同步代碼,將宏任務(wù)(Tasks)和微任務(wù)(Microtasks)劃分到各自隊(duì)列中君丁。

第二次執(zhí)行:

Tasks:run script枫夺、 setTimeout callback

Microtasks:Promise2 then    

JS stack: Promise2 callback 
Log: script start、script end绘闷、promise1橡庞、promise2
復(fù)制代碼

執(zhí)行宏任務(wù)后较坛,檢測(cè)到微任務(wù)(Microtasks)隊(duì)列中不為空,執(zhí)行Promise1扒最,執(zhí)行完成Promise1后丑勤,調(diào)用Promise2.then,放入微任務(wù)(Microtasks)隊(duì)列中吧趣,再執(zhí)行Promise2.then法竞。

第三次執(zhí)行:

Tasks:setTimeout callback

Microtasks: 

JS stack: setTimeout callback
Log: script start、script end再菊、promise1爪喘、promise2、setTimeout
復(fù)制代碼

當(dāng)微任務(wù)(Microtasks)隊(duì)列中為空時(shí)纠拔,執(zhí)行宏任務(wù)(Tasks)秉剑,執(zhí)行setTimeout callback,打印日志稠诲。

第四次執(zhí)行:

Tasks:setTimeout callback

Microtasks: 

JS stack: 
Log: script start侦鹏、script end、promise1臀叙、promise2略水、setTimeout
復(fù)制代碼

清空Tasks隊(duì)列和JS stack

以上執(zhí)行幀動(dòng)畫(huà)可以查看Tasks, microtasks, queues and schedules
或許這張圖也更好理解些劝萤。

[圖片上傳中...(image-9d9c28-1616378768946-5)]

<figcaption></figcaption>

再舉個(gè)例子

console.log('script start')

async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end') 
}
async1()

setTimeout(function() {
  console.log('setTimeout')
}, 0)

new Promise(resolve => {
  console.log('Promise')
  resolve()
})
  .then(function() {
    console.log('promise1')
  })
  .then(function() {
    console.log('promise2')
  })

console.log('script end')
復(fù)制代碼

這里需要先理解async/await渊涝。

async/await 在底層轉(zhuǎn)換成了 promisethen 回調(diào)函數(shù)。
也就是說(shuō)床嫌,這是 promise 的語(yǔ)法糖跨释。
每次我們使用 await, 解釋器都創(chuàng)建一個(gè) promise 對(duì)象,然后把剩下的 async 函數(shù)中的操作放到 then 回調(diào)函數(shù)中厌处。
async/await 的實(shí)現(xiàn)鳖谈,離不開(kāi) Promise。從字面意思來(lái)理解阔涉,async 是“異步”的簡(jiǎn)寫(xiě)缆娃,而 awaitasync wait 的簡(jiǎn)寫(xiě)可以認(rèn)為是等待異步方法執(zhí)行完成。

關(guān)于73以下版本和73版本的區(qū)別

  • 在老版本版本以下瑰排,先執(zhí)行promise1promise2贯要,再執(zhí)行async1
  • 在73版本椭住,先執(zhí)行async1再執(zhí)行promise1promise2郭毕。

主要原因是因?yàn)樵诠雀?金絲雀)73版本中更改了規(guī)范,如下圖所示:

[圖片上傳中...(image-ef63d6-1616378768945-4)]

<figcaption></figcaption>

  • 區(qū)別在于RESOLVE(thenable)和之間的區(qū)別Promise.resolve(thenable)函荣。

在老版本中

  • 首先显押,傳遞給 await 的值被包裹在一個(gè) Promise 中。然后傻挂,處理程序附加到這個(gè)包裝的 Promise乘碑,以便在 Promise 變?yōu)?fulfilled 后恢復(fù)該函數(shù),并且暫停執(zhí)行異步函數(shù)金拒,一旦 promise 變?yōu)?fulfilled兽肤,恢復(fù)異步函數(shù)的執(zhí)行。
  • 每個(gè) await 引擎必須創(chuàng)建兩個(gè)額外的 Promise(即使右側(cè)已經(jīng)是一個(gè) Promise)并且它需要至少三個(gè) microtask 隊(duì)列 tickstick為系統(tǒng)的相對(duì)時(shí)間單位绪抛,也被稱為系統(tǒng)的時(shí)基资铡,來(lái)源于定時(shí)器的周期性中斷(輸出脈沖),一次中斷表示一個(gè)tick幢码,也被稱做一個(gè)“時(shí)鐘滴答”笤休、時(shí)標(biāo)。)症副。

引用賀老師知乎上的一個(gè)例子

async function f() {
  await p
  console.log('ok')
}
復(fù)制代碼

簡(jiǎn)化理解為:


function f() {
  return RESOLVE(p).then(() => {
    console.log('ok')
  })
}
復(fù)制代碼
  • 如果 RESOLVE(p) 對(duì)于 ppromise 直接返回 p 的話店雅,那么 pthen 方法就會(huì)被馬上調(diào)用,其回調(diào)就立即進(jìn)入 job 隊(duì)列贞铣。
  • 而如果 RESOLVE(p) 嚴(yán)格按照標(biāo)準(zhǔn)闹啦,應(yīng)該是產(chǎn)生一個(gè)新的 promise,盡管該 promise確定會(huì) resolvep辕坝,但這個(gè)過(guò)程本身是異步的窍奋,也就是現(xiàn)在進(jìn)入 job 隊(duì)列的是新 promiseresolve過(guò)程,所以該 promisethen 不會(huì)被立即調(diào)用酱畅,而要等到當(dāng)前 job 隊(duì)列執(zhí)行到前述 resolve 過(guò)程才會(huì)被調(diào)用琳袄,然后其回調(diào)(也就是繼續(xù) await 之后的語(yǔ)句)才加入 job 隊(duì)列,所以時(shí)序上就晚了圣贸。

谷歌(金絲雀)73版本中

  • 使用對(duì)PromiseResolve的調(diào)用來(lái)更改await的語(yǔ)義挚歧,以減少在公共awaitPromise情況下的轉(zhuǎn)換次數(shù)。
  • 如果傳遞給 await 的值已經(jīng)是一個(gè) Promise吁峻,那么這種優(yōu)化避免了再次創(chuàng)建 Promise 包裝器滑负,在這種情況下,我們從最少三個(gè) microtick 到只有一個(gè) microtick用含。

詳細(xì)過(guò)程:

73以下版本

  • 首先矮慕,打印script start,調(diào)用async1()時(shí)啄骇,返回一個(gè)Promise痴鳄,所以打印出來(lái)async2 end
  • 每個(gè) await缸夹,會(huì)新產(chǎn)生一個(gè)promise,但這個(gè)過(guò)程本身是異步的痪寻,所以該await后面不會(huì)立即調(diào)用螺句。
  • 繼續(xù)執(zhí)行同步代碼,打印Promisescript end橡类,將then函數(shù)放入微任務(wù)隊(duì)列中等待執(zhí)行蛇尚。
  • 同步執(zhí)行完成之后,檢查微任務(wù)隊(duì)列是否為null顾画,然后按照先入先出規(guī)則取劫,依次執(zhí)行。
  • 然后先執(zhí)行打印promise1,此時(shí)then的回調(diào)函數(shù)返回undefinde研侣,此時(shí)又有then的鏈?zhǔn)秸{(diào)用谱邪,又放入微任務(wù)隊(duì)列中,再次打印promise2庶诡。
  • 再回到await的位置執(zhí)行返回的 Promiseresolve 函數(shù)惦银,這又會(huì)把 resolve 丟到微任務(wù)隊(duì)列中,打印async1 end灌砖。
  • 當(dāng)微任務(wù)隊(duì)列為空時(shí)璧函,執(zhí)行宏任務(wù),打印setTimeout

谷歌(金絲雀73版本)

  • 如果傳遞給 await 的值已經(jīng)是一個(gè) Promise基显,那么這種優(yōu)化避免了再次創(chuàng)建 Promise 包裝器蘸吓,在這種情況下,我們從最少三個(gè) microtick 到只有一個(gè) microtick撩幽。
  • 引擎不再需要為 await 創(chuàng)造 throwaway Promise - 在絕大部分時(shí)間库继。
  • 現(xiàn)在 promise 指向了同一個(gè) Promise,所以這個(gè)步驟什么也不需要做窜醉。然后引擎繼續(xù)像以前一樣宪萄,創(chuàng)建 throwaway Promise,安排 PromiseReactionJobmicrotask 隊(duì)列的下一個(gè) tick 上恢復(fù)異步函數(shù)榨惰,暫停執(zhí)行該函數(shù)拜英,然后返回給調(diào)用者。

具體詳情查看(這里)琅催。

NodeJS的Event Loop

[圖片上傳中...(image-ceeb-1616378768945-3)]

<figcaption></figcaption>

Node中的Event Loop是基于libuv實(shí)現(xiàn)的居凶,而libuvNode 的新跨平臺(tái)抽象層,libuv使用異步藤抡,事件驅(qū)動(dòng)的編程方式侠碧,核心是提供i/o的事件循環(huán)和異步回調(diào)。libuv的API包含有時(shí)間缠黍,非阻塞的網(wǎng)絡(luò)弄兜,異步文件操作,子進(jìn)程等等。 Event Loop就是在libuv中實(shí)現(xiàn)的替饿。

[圖片上傳中...(image-a4dcfc-1616378768945-2)]

<figcaption></figcaption>

NodeEvent loop一共分為6個(gè)階段语泽,每個(gè)細(xì)節(jié)具體如下:

  • timers: 執(zhí)行setTimeoutsetInterval中到期的callback
  • pending callback: 上一輪循環(huán)中少數(shù)的callback會(huì)放在這一階段執(zhí)行盛垦。
  • idle, prepare: 僅在內(nèi)部使用湿弦。
  • poll: 最重要的階段,執(zhí)行pending callback腾夯,在適當(dāng)?shù)那闆r下回阻塞在這個(gè)階段。
  • check: 執(zhí)行setImmediate(setImmediate()是將事件插入到事件隊(duì)列尾部蔬充,主線程和事件隊(duì)列的函數(shù)執(zhí)行完成之后立即執(zhí)行setImmediate指定的回調(diào)函數(shù))的callback蝶俱。
  • close callbacks: 執(zhí)行close事件的callback,例如socket.on('close'[,fn])或者http.server.on('close, fn)饥漫。

具體細(xì)節(jié)如下:

timers

執(zhí)行setTimeoutsetInterval中到期的callback榨呆,執(zhí)行這兩者回調(diào)需要設(shè)置一個(gè)毫秒數(shù),理論上來(lái)說(shuō)庸队,應(yīng)該是時(shí)間一到就立即執(zhí)行callback回調(diào)积蜻,但是由于system的調(diào)度可能會(huì)延時(shí),達(dá)不到預(yù)期時(shí)間彻消。
以下是官網(wǎng)文檔解釋的例子:

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
  }
});
復(fù)制代碼

當(dāng)進(jìn)入事件循環(huán)時(shí)竿拆,它有一個(gè)空隊(duì)列(fs.readFile()尚未完成),因此定時(shí)器將等待剩余毫秒數(shù)宾尚,當(dāng)?shù)竭_(dá)95ms時(shí)丙笋,fs.readFile()完成讀取文件并且其完成需要10毫秒的回調(diào)被添加到輪詢隊(duì)列并執(zhí)行。
當(dāng)回調(diào)結(jié)束時(shí)煌贴,隊(duì)列中不再有回調(diào)御板,因此事件循環(huán)將看到已達(dá)到最快定時(shí)器的閾值,然后回到timers階段以執(zhí)行定時(shí)器的回調(diào)牛郑。

在此示例中怠肋,您將看到正在調(diào)度的計(jì)時(shí)器與正在執(zhí)行的回調(diào)之間的總延遲將為105毫秒。

以下是我測(cè)試時(shí)間:

[圖片上傳中...(image-696d42-1616378768945-1)]

<figcaption></figcaption>

pending callbacks

此階段執(zhí)行某些系統(tǒng)操作(例如TCP錯(cuò)誤類型)的回調(diào)淹朋。 例如笙各,如果TCP socket ECONNREFUSED在嘗試connect時(shí)receives,則某些* nix系統(tǒng)希望等待報(bào)告錯(cuò)誤瑞你。 這將在pending callbacks階段執(zhí)行酪惭。

poll

該poll階段有兩個(gè)主要功能:

  • 執(zhí)行I/O回調(diào)。
  • 處理輪詢隊(duì)列中的事件者甲。

當(dāng)事件循環(huán)進(jìn)入poll階段并且在timers中沒(méi)有可以執(zhí)行定時(shí)器時(shí)春感,將發(fā)生以下兩種情況之一

  • 如果poll隊(duì)列不為空,則事件循環(huán)將遍歷其同步執(zhí)行它們的callback隊(duì)列,直到隊(duì)列為空鲫懒,或者達(dá)到system-dependent(系統(tǒng)相關(guān)限制)嫩实。

如果poll隊(duì)列為空,則會(huì)發(fā)生以下兩種情況之一

  • 如果有setImmediate()回調(diào)需要執(zhí)行窥岩,則會(huì)立即停止執(zhí)行poll階段并進(jìn)入執(zhí)行check階段以執(zhí)行回調(diào)甲献。

  • 如果沒(méi)有setImmediate()回到需要執(zhí)行,poll階段將等待callback被添加到隊(duì)列中颂翼,然后立即執(zhí)行晃洒。

當(dāng)然設(shè)定了 timer 的話且 poll 隊(duì)列為空,則會(huì)判斷是否有 timer 超時(shí)朦乏,如果有的話會(huì)回到 timer 階段執(zhí)行回調(diào)球及。

check

此階段允許人員在poll階段完成后立即執(zhí)行回調(diào)。
如果poll階段閑置并且script已排隊(duì)setImmediate()呻疹,則事件循環(huán)到達(dá)check階段執(zhí)行而不是繼續(xù)等待吃引。

setImmediate()實(shí)際上是一個(gè)特殊的計(jì)時(shí)器,它在事件循環(huán)的一個(gè)單獨(dú)階段運(yùn)行刽锤。它使用libuv API來(lái)調(diào)度在poll階段完成后執(zhí)行的回調(diào)镊尺。

通常,當(dāng)代碼被執(zhí)行時(shí)并思,事件循環(huán)最終將達(dá)到poll階段庐氮,它將等待傳入連接,請(qǐng)求等纺荧。
但是旭愧,如果已經(jīng)調(diào)度了回調(diào)setImmediate(),并且輪詢階段變?yōu)榭臻e宙暇,則它將結(jié)束并且到達(dá)check階段输枯,而不是等待poll事件。

console.log('start')
setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(function() {
    console.log('promise1')
  })
}, 0)
setTimeout(() => {
  console.log('timer2')
  Promise.resolve().then(function() {
    console.log('promise2')
  })
}, 0)
Promise.resolve().then(function() {
  console.log('promise3')
})
console.log('end')
復(fù)制代碼

如果node版本為v11.x占贫, 其結(jié)果與瀏覽器一致桃熄。

start
end
promise3
timer1
promise1
timer2
promise2

復(fù)制代碼

具體詳情可以查看《又被node的eventloop坑了,這次是node的鍋》型奥。

如果v10版本上述結(jié)果存在兩種情況:

  • 如果time2定時(shí)器已經(jīng)在執(zhí)行隊(duì)列中了
start
end
promise3
timer1
timer2
promise1
promise2
復(fù)制代碼
  • 如果time2定時(shí)器沒(méi)有在執(zhí)行對(duì)列中瞳收,執(zhí)行結(jié)果為
start
end
promise3
timer1
promise1
timer2
promise2
復(fù)制代碼

具體情況可以參考poll階段的兩種情況。

從下圖可能更好理解:

[圖片上傳中...(image-fd4935-1616378768944-0)]

<figcaption></figcaption>

setImmediate() 的setTimeout()的區(qū)別

setImmediatesetTimeout()是相似的厢汹,但根據(jù)它們被調(diào)用的時(shí)間以不同的方式表現(xiàn)螟深。

  • setImmediate()設(shè)計(jì)用于在當(dāng)前poll階段完成后check階段執(zhí)行腳本 。
  • setTimeout() 安排在經(jīng)過(guò)最刑淘帷(ms)后運(yùn)行的腳本界弧,在timers階段執(zhí)行凡蜻。

舉個(gè)例子

setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});
復(fù)制代碼

執(zhí)行定時(shí)器的順序?qū)⒏鶕?jù)調(diào)用它們的上下文而有所不同。 如果從主模塊中調(diào)用兩者垢箕,那么時(shí)間將受到進(jìn)程性能的限制划栓。

其結(jié)果也不一致

如果在I / O周期內(nèi)移動(dòng)兩個(gè)調(diào)用,則始終首先執(zhí)行立即回調(diào):

const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
復(fù)制代碼

其結(jié)果可以確定一定是immediate => timeout条获。
主要原因是在I/O階段讀取文件后忠荞,事件循環(huán)會(huì)先進(jìn)入poll階段,發(fā)現(xiàn)有setImmediate需要執(zhí)行帅掘,會(huì)立即進(jìn)入check階段執(zhí)行setImmediate的回調(diào)委煤。

然后再進(jìn)入timers階段,執(zhí)行setTimeout锄开,打印timeout素标。

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘
復(fù)制代碼

Process.nextTick()

process.nextTick()雖然它是異步API的一部分,但未在圖中顯示萍悴。這是因?yàn)?code>process.nextTick()從技術(shù)上講,它不是事件循環(huán)的一部分寓免。

  • process.nextTick()方法將 callback 添加到next tick隊(duì)列癣诱。 一旦當(dāng)前事件輪詢隊(duì)列的任務(wù)全部完成,在next tick隊(duì)列中的所有callbacks會(huì)被依次調(diào)用袜香。

換種理解方式:

  • 當(dāng)每個(gè)階段完成后撕予,如果存在 nextTick 隊(duì)列,就會(huì)清空隊(duì)列中的所有回調(diào)函數(shù)蜈首,并且優(yōu)先于其他 microtask 執(zhí)行实抡。

例子

let bar;

setTimeout(() => {
  console.log('setTimeout');
}, 0)

setImmediate(() => {
  console.log('setImmediate');
})
function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log('bar', bar); // 1
});

bar = 1;
復(fù)制代碼

在NodeV10中上述代碼執(zhí)行可能有兩種答案,一種為:

bar 1
setTimeout
setImmediate
復(fù)制代碼

另一種為:

bar 1
setImmediate
setTimeout
復(fù)制代碼

無(wú)論哪種欢策,始終都是先執(zhí)行process.nextTick(callback)吆寨,打印bar 1

最后

感謝@Dante_Hu提出這個(gè)問(wèn)題await的問(wèn)題踩寇,文章已經(jīng)修正啄清。 修改了node端執(zhí)行結(jié)果。V10和V11的區(qū)別俺孙。

關(guān)于await問(wèn)題參考了以下文章:.

promise, async, await, execution order
Normative: Reduce the number of ticks in async/await
async/await 在chrome 環(huán)境和 node 環(huán)境的 執(zhí)行結(jié)果不一致辣卒,求解?
更快的異步函數(shù)和 Promise

其他內(nèi)容參考了:

JS瀏覽器事件循環(huán)機(jī)制
什么是瀏覽器的事件循環(huán)(Event Loop)睛榄?
一篇文章教會(huì)你Event loop——瀏覽器和Node
不要混淆nodejs和瀏覽器中的event loop
瀏覽器與Node的事件循環(huán)(Event Loop)有何區(qū)別?
Tasks, microtasks, queues and schedules
前端面試之道
Node.js介紹5-libuv的基本概念
The Node.js Event Loop, Timers, and process.nextTick()
node官網(wǎng)

作者:光光同學(xué)22167
鏈接:https://juejin.cn/post/6844903764202094606
來(lái)源:掘金
著作權(quán)歸作者所有荣茫。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處场靴。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末啡莉,一起剝皮案震驚了整個(gè)濱河市港准,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌票罐,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奠蹬,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡冀痕,警方通過(guò)查閱死者的電腦和手機(jī)言蛇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)满哪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)民宿,“玉大人勘高,你說(shuō)我怎么就攤上這事坟桅”鲎ィ” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵招驴,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng)宾娜,這世上最難降的妖魔是什么困乒? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任娜搂,我火速辦了婚禮迁霎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘百宇。我一直安慰自己考廉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布携御。 她就那樣靜靜地躺著昌粤,像睡著了一般既绕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上涮坐,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天凄贩,我揣著相機(jī)與錄音,去河邊找鬼袱讹。 笑死疲扎,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的廓译。 我是一名探鬼主播评肆,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼非区!你這毒婦竟也來(lái)了瓜挽?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤征绸,失蹤者是張志新(化名)和其女友劉穎久橙,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體管怠,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡淆衷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了渤弛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片祝拯。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖她肯,靈堂內(nèi)的尸體忽然破棺而出佳头,到底是詐尸還是另有隱情,我是刑警寧澤晴氨,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布康嘉,位于F島的核電站,受9級(jí)特大地震影響籽前,放射性物質(zhì)發(fā)生泄漏亭珍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一枝哄、第九天 我趴在偏房一處隱蔽的房頂上張望肄梨。 院中可真熱鬧,春花似錦膘格、人聲如沸峭范。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)纱控。三九已至辆毡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間甜害,已是汗流浹背舶掖。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留尔店,地道東北人眨攘。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像嚣州,于是被迫代替她去往敵國(guó)和親鲫售。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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

  • 前言 Event Loop即事件循環(huán)该肴,是指瀏覽器或Node的一種解決javaScript單線程運(yùn)行時(shí)不會(huì)阻塞的一種...
    優(yōu)秀的javaScript閱讀 1,019評(píng)論 0 2
  • 前言 Event Loop即事件循環(huán)情竹,是指瀏覽器或Node的一種解決javaScript單線程運(yùn)行時(shí)不會(huì)阻塞的一種...
    CodeMT閱讀 408評(píng)論 0 0
  • 事件循環(huán)Event Loop JavaScript語(yǔ)言的一大特點(diǎn)就是單線程,作為腳本語(yǔ)言匀哄,避免復(fù)雜性秦效。因?yàn)槿绻嵌?..
    ERICOOLU閱讀 381評(píng)論 0 1
  • 瀏覽器中的 Event loop JavaScript 是單線程的 首先,語(yǔ)言產(chǎn)生的時(shí)代多進(jìn)程多線程的架構(gòu)并不普...
    _1633_閱讀 179評(píng)論 0 1
  • 前言 Event Loop即事件循環(huán)涎嚼,是指瀏覽器或Node的一種解決javaScript單線程運(yùn)行時(shí)不會(huì)阻塞的一種...
    六月繁花開(kāi)閱讀 447評(píng)論 0 3