# Event Loop詳解
點擊關注本[公眾號](http://www.hmoore.net/book/dsh225/javascript_vue_css/edit#_118)獲取文檔最新更新,并可以領取配套于本指南的《**前端面試手冊**》以及**最標準的簡歷模板**.
> 本文是[弄懂Event Loop](https://juejin.im/post/5c3d8956e51d4511dc72c200?utm_source=gold_browser_extension#comment)的刪改版,去除了原文中一些容易引起歧義的部分,對一些內容進行了擴充
[TOC]
## 前言
`Event Loop`即事件循環,是指瀏覽器或`Node`的一種解決`javaScript`單線程運行時不會阻塞的一種機制,也就是我們經常使用**異步**的原理。
## 為啥要弄懂Event Loop
* 是要增加自己技術的深度,也就是懂得`JavaScript`的運行機制。
* 現在在前端領域各種技術層出不窮,掌握底層原理,可以讓自己以不變,應萬變。
* 應對各大互聯網公司的面試,懂其原理,題目任其發揮。
## 棧、隊列的基本概念

### 棧(Stack)
**棧**在計算機科學中是限定僅在**表尾**進行**插入**或**刪除**操作的線性表。?**棧**是一種數據結構,它按照**后進先出**的原則存儲數據,**先進入**的數據被壓入**棧底**,**最后的數據**在**棧頂**,需要讀數據的時候從**棧頂**開始**彈出數據**。
**棧**是只能在**某一端插入**和**刪除**的**特殊線性表**。

### 隊列(Queue)
特殊之處在于它只允許在表的前端(`front`)進行**刪除**操作,而在表的后端(`rear`)進行**插入**操作,和**棧**一樣,**隊列**是一種操作受限制的線性表。
進行**插入**操作的端稱為**隊尾**,進行**刪除**操作的端稱為**隊頭**。 隊列中沒有元素時,稱為**空隊列**。
**隊列**的數據元素又稱為**隊列元素**。在隊列中插入一個隊列元素稱為**入隊**,從**隊列**中**刪除**一個隊列元素稱為**出隊**。因為隊列**只允許**在一端**插入**,在另一端**刪除**,所以只有**最早**進入**隊列**的元素**才能最先從隊列中**刪除,故隊列又稱為**先進先出**(`FIFO—first in first out`)

## Event Loop
在`JavaScript`中,任務被分為兩種,一種宏任務(`MacroTask`)也叫`Task`,一種叫微任務(`MicroTask`)。
### (宏任務)
* `script`全部代碼、`setTimeout`、`setInterval`、`setImmediate`(瀏覽器暫時不支持,只有IE10支持,具體可見[`MDN`](https://link.juejin.im/?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FAPI%2FWindow%2FsetImmediate))、`I/O`、`UI Rendering`。
### (微任務)
* `Process.nextTick(Node獨有)`、`Promise`、`Object.observe(廢棄)`、`MutationObserver`(具體使用方式查看[這里](https://link.juejin.im/?target=http%3A%2F%2Fjavascript.ruanyifeng.com%2Fdom%2Fmutationobserver.html))
## 瀏覽器中的Event Loop
`Javascript`?有一個?`main thread`?主線程和?`call-stack`?調用棧(執行棧),所有的任務都會被放到調用棧等待主線程執行。
### JS調用棧
JS調用棧采用的是后進先出的規則,當函數執行的時候,會被添加到棧的頂部,當執行棧執行完成后,就會從棧頂移出,直到棧內被清空。
### 同步任務和異步任務
`Javascript`單線程任務被分為**同步任務**和**異步任務**,同步任務會在調用棧中按照順序等待主線程依次執行,異步任務會在異步任務有了結果后,將注冊的回調函數放入任務隊列中等待主線程空閑的時候(調用棧被清空),被讀取到棧內等待主線程的執行。
任務隊列`Task Queue`,即隊列,是一種先進先出的一種數據結構。
### 事件循環的進程模型
* 選擇當前要執行的任務隊列,選擇任務隊列中最先進入的任務,如果任務隊列為空即`null`,則執行跳轉到微任務(`MicroTask`)的執行步驟。
* 將事件循環中的任務設置為已選擇任務。
* 執行任務。
* 將事件循環中當前運行任務設置為null。
* 將已經運行完成的任務從任務隊列中刪除。
* microtasks步驟:進入microtask檢查點。
* 更新界面渲染。
* 返回第一步。
### 執行進入microtask檢查點時,用戶代理會執行以下步驟:
* 設置microtask檢查點標志為true。
* 當事件循環`microtask`執行不為空時:選擇一個最先進入的`microtask`隊列的`microtask`,將事件循環的`microtask`設置為已選擇的`microtask`,運行`microtask`,將已經執行完成的`microtask`為`null`,移出`microtask`中的`microtask`。
* 清理IndexDB事務
* 設置進入microtask檢查點的標志為false。
上述可能不太好理解,下圖是我做的一張圖片。

執行棧在執行完**同步任務**后,查看**執行棧**是否為空,如果執行棧為空,就會去檢查**微任務**(`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、promise1、promise2
~~~
執行宏任務后,檢測到微任務(`Microtasks`)隊列中不為空,執行`Promise1`,執行完成`Promise1`后,調用`Promise2.then`,放入微任務(`Microtasks`)隊列中,再執行`Promise2.then`。
### 第三次執行:
~~~
Tasks:setTimeout callback
Microtasks:
JS stack: setTimeout callback
Log: script start、script end、promise1、promise2、setTimeout
~~~
當微任務(`Microtasks`)隊列中為空時,執行宏任務(`Tasks`),執行`setTimeout callback`,打印日志。
### 第四次執行:
~~~
Tasks:setTimeout callback
Microtasks:
JS stack:
Log: script start、script end、promise1、promise2、setTimeout
~~~
清空**Tasks**隊列和`JS stack`。
以上執行幀動畫可以查看[Tasks, microtasks, queues and schedules](https://link.juejin.im/?target=https%3A%2F%2Fjakearchibald.com%2F2015%2Ftasks-microtasks-queues-and-schedules%2F)
或許這張圖也更好理解些。

## 再舉個例子
~~~
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`?的簡寫可以認為是等待異步方法執行完成。
### **關于73以下版本和73版本的區別**
* 在老版本版本以下,先執行`promise1`和`promise2`,再執行`async1`。
* 在73版本,先執行`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`?隊列,所以時序上就晚了。
### **谷歌(金絲雀)73版本中**
* 使用對`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`?上恢復異步函數,暫停執行該函數,然后返回給調用者。
具體詳情查看([這里](https://link.juejin.im/?target=https%3A%2F%2Fv8.js.cn%2Fblog%2Ffast-async%2F))。
## NodeJS的Event Loop

`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)`。
具體細節如下:
### timers
執行`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毫秒。
**以下是我測試時間:**

### pending callbacks
此階段執行某些系統操作(例如TCP錯誤類型)的回調。 例如,如果`TCP socket ECONNREFUSED`在嘗試connect時receives,則某些\* nix系統希望等待報告錯誤。 這將在`pending callbacks`階段執行。
### poll
**該poll階段有兩個主要功能:**
* 執行`I/O`回調。
* 處理輪詢隊列中的事件。
**當事件循環進入`poll`階段并且在`timers`中沒有可以執行定時器時,將發生以下兩種情況之一**
* 如果`poll`隊列不為空,則事件循環將遍歷其同步執行它們的`callback`隊列,直到隊列為空,或者達到`system-dependent`(系統相關限制)。
**如果`poll`隊列為空,則會發生以下兩種情況之一**
* 如果有`setImmediate()`回調需要執行,則會立即停止執行`poll`階段并進入執行`check`階段以執行回調。
* 如果沒有`setImmediate()`回到需要執行,poll階段將等待`callback`被添加到隊列中,然后立即執行。
**當然設定了 timer 的話且 poll 隊列為空,則會判斷是否有 timer 超時,如果有的話會回到 timer 階段執行回調。**
### check
**此階段允許人員在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的鍋](https://juejin.im/post/5c3e8d90f265da614274218a)》。
如果v10版本上述結果存在兩種情況:
* 如果time2定時器已經在執行隊列中了
~~~
start
end
promise3
timer1
timer2
promise1
promise2
~~~
* 如果time2定時器沒有在執行對列中,執行結果為
~~~
start
end
promise3
timer1
promise1
timer2
promise2
~~~
具體情況可以參考`poll`階段的兩種情況。
從下圖可能更好理解:

## setImmediate() 的setTimeout()的區別
**`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()
**`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`。
* * *
## 公眾號
想要實時關注筆者最新的文章和最新的文檔更新請關注公眾號**程序員面試官**,后續的文章會優先在公眾號更新.
**簡歷模板**:關注公眾號回復「模板」獲取
《**前端面試手冊**》:配套于本指南的突擊手冊,關注公眾號回復「fed」獲取

- 前言
- 指南使用手冊
- 為什么會有這個項目
- 面試技巧
- 面試官到底想看什么樣的簡歷?
- 面試回答問題的技巧
- 如何通過HR面
- 推薦
- 書籍/課程推薦
- 前端基礎
- HTML基礎
- CSS基礎
- JavaScript基礎
- 瀏覽器與新技術
- DOM
- 前端基礎筆試
- HTTP筆試部分
- JavaScript筆試部分
- 前端原理詳解
- JavaScript的『預解釋』與『變量提升』
- Event Loop詳解
- 實現不可變數據
- JavaScript內存管理
- 實現深克隆
- 如何實現一個Event
- JavaScript的運行機制
- 計算機基礎
- HTTP協議
- TCP面試題
- 進程與線程
- 數據結構與算法
- 算法面試題
- 字符串類面試題
- 前端框架
- 關于前端框架的面試須知
- Vue面試題
- React面試題
- 框架原理詳解
- 虛擬DOM原理
- Proxy比defineproperty優劣對比?
- setState到底是異步的還是同步的?
- 前端路由的實現
- redux原理全解
- React Fiber 架構解析
- React組件復用指南
- React-hooks 抽象組件
- 框架實戰技巧
- 如何搭建一個組件庫的開發環境
- 組件設計原則
- 實現輪播圖組件
- 性能優化
- 前端性能優化-加載篇
- 前端性能優化-執行篇
- 工程化
- webpack面試題
- 前端工程化
- Vite
- 安全
- 前端安全面試題
- npm
- 工程化原理
- 如何寫一個babel
- Webpack HMR 原理解析
- webpack插件編寫
- webpack 插件化設計
- Webpack 模塊機制
- webpack loader實現
- 如何開發Babel插件
- git
- 比較
- 查看遠程倉庫地址
- git flow
- 比較分支的不同并保存壓縮文件
- Tag
- 回退
- 前端項目經驗
- 確定用戶是否在當前頁面
- 前端下載文件
- 只能在微信中訪問
- 打開新頁面-被瀏覽器攔截
- textarea高度隨內容變化 vue版
- 去掉ios原始播放大按鈕
- nginx在MAC上的安裝、啟動、重啟和關閉
- 解析latex格式的數學公式
- 正則-格式化a鏈接
- 封裝的JQ插件庫
- 打包問題總結
- NPM UI插件
- 帶你入門前端工程
- webWorker+indexedDB性能優化
- 多個相鄰元素切換效果出現邊框重疊問題的解決方法
- 監聽前端storage變化