Event Loop
即事件循環,是指瀏覽器或Node
的一種解決javaScript
單線程運行時不會阻塞的一種機制,也就是咱們常用異步的原理。javascript
是要增長本身技術的深度,也就是懂得JavaScript
的運行機制。html
如今在前端領域各類技術層出不窮,掌握底層原理,可讓本身以不變,應萬變。前端
應對各大互聯網公司的面試,懂其原理,題目任其發揮。java
堆是一種數據結構,是利用徹底二叉樹維護的一組數據,堆分爲兩種,一種爲最大堆,一種爲最小堆,將根節點最大的堆叫作最大堆或大根堆,根節點最小的堆叫作最小堆或小根堆。
堆是線性數據結構,至關於一維數組,有惟一後繼。node
如最大堆git
棧在計算機科學中是限定僅在表尾進行插入或刪除操做的線性表。 棧是一種數據結構,它按照後進先出的原則存儲數據,先進入的數據被壓入棧底,最後的數據在棧頂,須要讀數據的時候從棧頂開始彈出數據。
棧是隻能在某一端插入和刪除的特殊線性表。github
特殊之處在於它只容許在表的前端(front
)進行刪除操做,而在表的後端(rear
)進行插入操做,和棧同樣,隊列是一種操做受限制的線性表。
進行插入操做的端稱爲隊尾,進行刪除操做的端稱爲隊頭。 隊列中沒有元素時,稱爲空隊列。面試
隊列的數據元素又稱爲隊列元素。在隊列中插入一個隊列元素稱爲入隊,從隊列中刪除一個隊列元素稱爲出隊。由於隊列只容許在一端插入,在另外一端刪除,因此只有最先進入隊列的元素才能最早從隊列中刪除,故隊列又稱爲先進先出(FIFO—first in first out
)chrome
在JavaScript
中,任務被分爲兩種,一種宏任務(MacroTask
)也叫Task
,一種叫微任務(MicroTask
)。編程
script
所有代碼、setTimeout
、setInterval
、setImmediate
(瀏覽器暫時不支持,只有IE10支持,具體可見MDN
)、I/O
、UI Rendering
。Process.nextTick(Node獨有)
、Promise
、Object.observe(廢棄)
、MutationObserver
(具體使用方式查看這裏)Javascript
有一個 main thread
主線程和 call-stack
調用棧(執行棧),全部的任務都會被放到調用棧等待主線程執行。
JS調用棧採用的是後進先出的規則,當函數執行的時候,會被添加到棧的頂部,當執行棧執行完成後,就會從棧頂移出,直到棧內被清空。
Javascript
單線程任務被分爲同步任務和異步任務,同步任務會在調用棧中按照順序等待主線程依次執行,異步任務會在異步任務有告終果後,將註冊的回調函數放入任務隊列中等待主線程空閒的時候(調用棧被清空),被讀取到棧內等待主線程的執行。
Task Queue
,即隊列,是一種先進先出的一種數據結構。
null
,則執行跳轉到微任務(MicroTask
)的執行步驟。microtask
執行不爲空時:選擇一個最早進入的microtask
隊列的microtask
,將事件循環的microtask
設置爲已選擇的microtask
,運行microtask
,將已經執行完成的microtask
爲null
,移出microtask
中的microtask
。上述可能不太好理解,下圖是我作的一張圖片。
執行棧在執行完同步任務後,查看執行棧是否爲空,若是執行棧爲空,就會去檢查微任務(microTask
)隊列是否爲空,若是爲空的話,就執行Task
(宏任務),不然就一次性執行完全部微任務。
每次單個宏任務執行完畢後,檢查微任務(microTask
)隊列是否爲空,若是不爲空的話,會按照先入先出的規則所有執行完微任務(microTask
)後,設置微任務(microTask
)隊列爲null
,而後再執行宏任務,如此循環。
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');
複製代碼
首先咱們劃分幾個分類:
Tasks:run script、 setTimeout callback
Microtasks:Promise then
JS stack: script
Log: script start、script end。
複製代碼
執行同步代碼,將宏任務(Tasks
)和微任務(Microtasks
)劃分到各自隊列中。
Tasks:run script、 setTimeout callback
Microtasks:Promise2 then
JS stack: Promise2 callback
Log: script start、script end、promise一、promise2
複製代碼
執行宏任務後,檢測到微任務(Microtasks
)隊列中不爲空,執行Promise1
,執行完成Promise1
後,調用Promise2.then
,放入微任務(Microtasks
)隊列中,再執行Promise2.then
。
Tasks:setTimeout callback
Microtasks:
JS stack: setTimeout callback
Log: script start、script end、promise一、promise二、setTimeout
複製代碼
當微任務(Microtasks
)隊列中爲空時,執行宏任務(Tasks
),執行setTimeout callback
,打印日誌。
Tasks:setTimeout callback
Microtasks:
JS stack:
Log: script start、script end、promise一、promise二、setTimeout
複製代碼
清空Tasks隊列和JS stack
。
以上執行幀動畫能夠查看Tasks, microtasks, queues and schedules
或許這張圖也更好理解些。
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')
複製代碼
這裏須要先理解async/await
。
async/await
在底層轉換成了 promise
和 then
回調函數。
也就是說,這是 promise
的語法糖。
每次咱們使用 await
, 解釋器都建立一個 promise
對象,而後把剩下的 async
函數中的操做放到 then
回調函數中。
async/await
的實現,離不開 Promise
。從字面意思來理解,async
是「異步」的簡寫,而 await
是 async wait
的簡寫能夠認爲是等待異步方法執行完成。
promise1
和promise2
,再執行async1
。async1
再執行promise1
和promise2
。主要緣由是由於在谷歌(金絲雀)73版本中更改了規範,以下圖所示:
RESOLVE(thenable)
和之間的區別Promise.resolve(thenable)
。await
的值被包裹在一個 Promise
中。而後,處理程序附加到這個包裝的 Promise
,以便在 Promise
變爲 fulfilled
後恢復該函數,而且暫停執行異步函數,一旦 promise
變爲 fulfilled
,恢復異步函數的執行。await
引擎必須建立兩個額外的 Promise(即便右側已是一個 Promise
)而且它須要至少三個 microtask
隊列 ticks
(tick
爲系統的相對時間單位,也被稱爲系統的時基,來源於定時器的週期性中斷(輸出脈衝),一次中斷表示一個tick
,也被稱作一個「時鐘滴答」、時標。)。async function f() {
await p
console.log('ok')
}
複製代碼
簡化理解爲:
function f() {
return RESOLVE(p).then(() => {
console.log('ok')
})
}
複製代碼
RESOLVE(p)
對於 p
爲 promise
直接返回 p
的話,那麼 p
的 then
方法就會被立刻調用,其回調就當即進入 job
隊列。RESOLVE(p)
嚴格按照標準,應該是產生一個新的 promise
,儘管該 promise
肯定會 resolve
爲 p
,但這個過程自己是異步的,也就是如今進入 job
隊列的是新 promise
的 resolve
過程,因此該 promise
的 then
不會被當即調用,而要等到當前 job
隊列執行到前述 resolve
過程纔會被調用,而後其回調(也就是繼續 await
以後的語句)才加入 job
隊列,因此時序上就晚了。PromiseResolve
的調用來更改await
的語義,以減小在公共awaitPromise
狀況下的轉換次數。await
的值已是一個 Promise
,那麼這種優化避免了再次建立 Promise
包裝器,在這種狀況下,咱們從最少三個 microtick
到只有一個 microtick
。73如下版本
script start
,調用async1()
時,返回一個Promise
,因此打印出來async2 end
。await
,會新產生一個promise
,但這個過程自己是異步的,因此該await
後面不會當即調用。Promise
和script end
,將then
函數放入微任務隊列中等待執行。null
,而後按照先入先出規則,依次執行。promise1
,此時then
的回調函數返回undefinde
,此時又有then
的鏈式調用,又放入微任務隊列中,再次打印promise2
。await
的位置執行返回的 Promise
的 resolve
函數,這又會把 resolve
丟到微任務隊列中,打印async1 end
。setTimeout
。谷歌(金絲雀73版本)
await
的值已是一個 Promise
,那麼這種優化避免了再次建立 Promise
包裝器,在這種狀況下,咱們從最少三個 microtick
到只有一個 microtick
。await
創造 throwaway Promise
- 在絕大部分時間。promise
指向了同一個 Promise
,因此這個步驟什麼也不須要作。而後引擎繼續像之前同樣,建立 throwaway Promise
,安排 PromiseReactionJob
在 microtask
隊列的下一個 tick
上恢復異步函數,暫停執行該函數,而後返回給調用者。具體詳情查看(這裏)。
Node
中的Event Loop
是基於libuv
實現的,而libuv
是 Node
的新跨平臺抽象層,libuv使用異步,事件驅動的編程方式,核心是提供i/o
的事件循環和異步回調。libuv的API
包含有時間,非阻塞的網絡,異步文件操做,子進程等等。 Event Loop
就是在libuv
中實現的。
Node
的Event loop
一共分爲6個階段,每一個細節具體以下:timers
: 執行setTimeout
和setInterval
中到期的callback
。pending callback
: 上一輪循環中少數的callback
會放在這一階段執行。idle, prepare
: 僅在內部使用。poll
: 最重要的階段,執行pending callback
,在適當的狀況下回阻塞在這個階段。check
: 執行setImmediate
(setImmediate()
是將事件插入到事件隊列尾部,主線程和事件隊列的函數執行完成以後當即執行setImmediate
指定的回調函數)的callback
。close callbacks
: 執行close
事件的callback
,例如socket.on('close'[,fn])
或者http.server.on('close, fn)
。具體細節以下:
執行setTimeout
和setInterval
中到期的callback
,執行這二者回調須要設置一個毫秒數,理論上來講,應該是時間一到就當即執行callback回調,可是因爲system
的調度可能會延時,達不到預期時間。
如下是官網文檔解釋的例子:
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
}
});
複製代碼
當進入事件循環時,它有一個空隊列(fs.readFile()
還沒有完成),所以定時器將等待剩餘毫秒數,當到達95ms時,fs.readFile()
完成讀取文件而且其完成須要10毫秒的回調被添加到輪詢隊列並執行。
當回調結束時,隊列中再也不有回調,所以事件循環將看到已達到最快定時器的閾值,而後回到timers階段以執行定時器的回調。
在此示例中,您將看到正在調度的計時器與正在執行的回調之間的總延遲將爲105毫秒。
如下是我測試時間:
此階段執行某些系統操做(例如TCP錯誤類型)的回調。 例如,若是TCP socket ECONNREFUSED
在嘗試connect時receives,則某些* nix系統但願等待報告錯誤。 這將在pending callbacks
階段執行。
該poll階段有兩個主要功能:
I/O
回調。當事件循環進入poll
階段而且在timers
中沒有能夠執行定時器時,將發生如下兩種狀況之一
poll
隊列不爲空,則事件循環將遍歷其同步執行它們的callback
隊列,直到隊列爲空,或者達到system-dependent
(系統相關限制)。若是poll
隊列爲空,則會發生如下兩種狀況之一
若是有setImmediate()
回調須要執行,則會當即中止執行poll
階段並進入執行check
階段以執行回調。
若是沒有setImmediate()
回到須要執行,poll階段將等待callback
被添加到隊列中,而後當即執行。
固然設定了 timer 的話且 poll 隊列爲空,則會判斷是否有 timer 超時,若是有的話會回到 timer 階段執行回調。
此階段容許人員在poll階段完成後當即執行回調。
若是poll
階段閒置而且script
已排隊setImmediate()
,則事件循環到達check階段執行而不是繼續等待。
setImmediate()
其實是一個特殊的計時器,它在事件循環的一個單獨階段運行。它使用libuv API
來調度在poll
階段完成後執行的回調。
一般,當代碼被執行時,事件循環最終將達到poll
階段,它將等待傳入鏈接,請求等。
可是,若是已經調度了回調setImmediate()
,而且輪詢階段變爲空閒,則它將結束而且到達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')
複製代碼
若是node
版本爲v11.x
, 其結果與瀏覽器一致。
start
end
promise3
timer1
promise1
timer2
promise2
複製代碼
具體詳情能夠查看《又被node的eventloop坑了,此次是node的鍋》。
若是v10版本上述結果存在兩種狀況:
start
end
promise3
timer1
timer2
promise1
promise2
複製代碼
start
end
promise3
timer1
promise1
timer2
promise2
複製代碼
具體狀況能夠參考poll
階段的兩種狀況。
從下圖可能更好理解:
setImmediate
和setTimeout()
是類似的,但根據它們被調用的時間以不一樣的方式表現。
setImmediate()
設計用於在當前poll
階段完成後check階段執行腳本 。setTimeout()
安排在通過最小(ms)後運行的腳本,在timers
階段執行。setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
複製代碼
執行定時器的順序將根據調用它們的上下文而有所不一樣。 若是從主模塊中調用二者,那麼時間將受到進程性能的限制。
其結果也不一致
若是在I / O
週期內移動兩個調用,則始終首先執行當即回調:
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
複製代碼
其結果能夠肯定必定是immediate => timeout
。
主要緣由是在I/O階段
讀取文件後,事件循環會先進入poll
階段,發現有setImmediate
須要執行,會當即進入check
階段執行setImmediate
的回調。
而後再進入timers
階段,執行setTimeout
,打印timeout
。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
複製代碼
process.nextTick()
雖然它是異步API的一部分,但未在圖中顯示。這是由於process.nextTick()
從技術上講,它不是事件循環的一部分。
process.nextTick()
方法將 callback
添加到next tick
隊列。 一旦當前事件輪詢隊列的任務所有完成,在next tick
隊列中的全部callbacks
會被依次調用。換種理解方式:
nextTick
隊列,就會清空隊列中的全部回調函數,而且優先於其餘 microtask
執行。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;
複製代碼
在NodeV10中上述代碼執行可能有兩種答案,一種爲:
bar 1
setTimeout
setImmediate
複製代碼
另外一種爲:
bar 1
setImmediate
setTimeout
複製代碼
不管哪一種,始終都是先執行process.nextTick(callback)
,打印bar 1
。
感謝@Dante_Hu提出這個問題await
的問題,文章已經修正。 修改了node端執行結果。V10和V11的區別。
《promise, async, await, execution order》
《Normative: Reduce the number of ticks in async/await》
《async/await 在chrome 環境和 node 環境的 執行結果不一致,求解?》
《更快的異步函數和 Promise》
《JS瀏覽器事件循環機制》
《什麼是瀏覽器的事件循環(Event Loop)?》
《一篇文章教會你Event loop——瀏覽器和Node》
《不要混淆nodejs和瀏覽器中的event loop》
《瀏覽器與Node的事件循環(Event Loop)有何區別?》
《Tasks, microtasks, queues and schedules》
《前端面試之道》
《Node.js介紹5-libuv的基本概念》
《The Node.js Event Loop, Timers, and process.nextTick()》
《node官網》