前言
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),是利用完全二叉樹維護(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í)候從棧頂開始彈出數(shù)據(jù)。
棧是只能在某一端插入和刪除的特殊線性表。
[圖片上傳中...(image-55a07b-1616378768948-10)]
<figcaption></figcaption>
隊(duì)列(Queue)
特殊之處在于它只允許在表的前端(front)進(jìn)行刪除操作,而在表的后端(rear)進(jìn)行插入操作,和棧一樣,隊(duì)列是一種操作受限制的線性表。
進(jìn)行插入操作的端稱為隊(duì)尾,進(jìn)行刪除操作的端稱為隊(duì)頭。 隊(duì)列中沒有元素時(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、setInterval、setImmediate(瀏覽器暫時(shí)不支持,只有IE10支持,具體可見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í)行完成的microtask為null,移出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)畫可以查看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)換成了 promise 和 then 回調(diào)函數(shù)。
也就是說(shuō),這是 promise 的語(yǔ)法糖。
每次我們使用 await, 解釋器都創(chuàng)建一個(gè) promise 對(duì)象,然后把剩下的 async 函數(shù)中的操作放到 then 回調(diào)函數(shù)中。
async/await 的實(shí)現(xiàn),離不開 Promise。從字面意思來(lái)理解,async 是“異步”的簡(jiǎn)寫,而 await 是 async wait 的簡(jiǎn)寫可以認(rèn)為是等待異步方法執(zhí)行完成。
關(guān)于73以下版本和73版本的區(qū)別
- 在老版本版本以下,先執(zhí)行
promise1和promise2,再執(zhí)行async1。 - 在73版本,先執(zhí)行
async1再執(zhí)行promise1和promise2。
主要原因是因?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ì)列ticks(tick為系統(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ì)于p為promise直接返回p的話,那么p的then方法就會(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ì)resolve為p,但這個(gè)過程本身是異步的,也就是現(xiàn)在進(jìn)入job隊(duì)列的是新promise的resolve過程,所以該promise的then不會(huì)被立即調(diào)用,而要等到當(dāng)前job隊(duì)列執(zhí)行到前述resolve過程才會(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ì)過程:
73以下版本
- 首先,打印
script start,調(diào)用async1()時(shí),返回一個(gè)Promise,所以打印出來(lái)async2 end。 - 每個(gè)
await,會(huì)新產(chǎn)生一個(gè)promise,但這個(gè)過程本身是異步的,所以該await后面不會(huì)立即調(diào)用。 - 繼續(xù)執(zhí)行同步代碼,打印
Promise和script 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í)行返回的Promise的resolve函數(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,安排PromiseReactionJob在microtask隊(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)的,而libuv是 Node 的新跨平臺(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>
Node的Event loop一共分為6個(gè)階段,每個(gè)細(xì)節(jié)具體如下:
-
timers: 執(zhí)行setTimeout和setInterval中到期的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í)行setTimeout和setInterval中到期的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中沒有可以執(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)。如果沒有
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í)器沒有在執(zhí)行對(duì)列中,執(zhí)行結(jié)果為
start
end
promise3
timer1
promise1
timer2
promise2
復(fù)制代碼
具體情況可以參考poll階段的兩種情況。
從下圖可能更好理解:
[圖片上傳中...(image-fd4935-1616378768944-0)]
<figcaption></figcaption>
setImmediate() 的setTimeout()的區(qū)別
setImmediate和setTimeout()是相似的,但根據(jù)它們被調(diào)用的時(shí)間以不同的方式表現(xiàn)。
-
setImmediate()設(shè)計(jì)用于在當(dāng)前poll階段完成后check階段執(zhí)行腳本 。 -
setTimeout()安排在經(jīng)過最?。╩s)后運(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è)問題await的問題,文章已經(jīng)修正。 修改了node端執(zhí)行結(jié)果。V10和V11的區(qū)別。
關(guān)于await問題參考了以下文章:.
《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)注明出處。