## 前言
`Event Loop`即事件循環,是瀏覽器或`Node`解決單線程運行時不會阻塞的一種機制。
在正式學習`Event Loop`之前,先需要解決幾個問題:
1. 什么是同步與異步?
2. `JavaScript`是一門單線程語言,那如何實現異步?
3. 同步任務和異步任務的執行順序如何?
4. 異步任務是否存在優先級?
## 同步與異步
計算機領域中的同步與異步和我們現實社會的同步和異步正好相反。現實中的同步,就是同時進行,突出的是"同",比如看足球比賽的時候吃著零食,兩件事情同時發生;異步就是不同時。但計算機中與現實存在一定差異。
### 舉個栗子
天氣冷了,早上剛醒來想喝點熱水暖暖身子,但這每天起早貪黑996,晚上回來太累躺下就睡,沒開水啊,沒法子,只好急急忙忙去燒水。
現在早上太冷了啊,不由得在被窩里面多躺了一會,收拾的時間緊緊巴巴,不能空等水開,于是我便趁此去洗漱,收拾自己。 洗漱完,水開了,喝到暖暖的熱水,舒服啊!
舒服完,開啟新的996之日,打工人出發!
燒水和洗漱是在同時間進行的,這就是**計算機中的異步**。
**計算機中的同步**是連續性的動作,上一步未完成前,下一步會發生堵塞,直至上一步完成后,下一步才可以繼續執行。例如:只有等水開,才能喝到暖暖的熱水。
## 單線程卻可以異步?
`JavaScript`的確是一門單線程語言,但是瀏覽器`UI`是多線程的,異步任務借助瀏覽器的線程和`JavaScript`的執行機制實現。 例如,`setTimeout`就借助瀏覽器定時器觸發線程的計時功能來實現。
### 瀏覽器線程
1. `GUI`渲染線程
* 繪制頁面,解析HTML、CSS,構建DOM樹等
* 頁面的重繪和重排
* 與JS引擎互斥(JS引擎阻塞頁面刷新)
2. `JS`引擎線程
* js腳本代碼執行
* 負責執行準備好的事件,例如定時器計時結束或異步請求成功且正確返回
* 與GUI渲染線程互斥
3. 事件觸發線程
* 當對應的事件滿足觸發條件,將事件添加到js的任務隊列末尾
* 多個事件加入任務隊列需要排隊等待
4. 定時器觸發線程
* 負責執行異步的定時器類事件:setTimeout、setInterval等
* 瀏覽器定時計時由該線程完成,計時完畢后將事件添加至任務隊列隊尾
5. `HTTP`請求線程
* 負責異步請求
* 當監聽到異步請求狀態變更時,如果存在回調函數,該線程會將回調函數加入到任務隊列隊尾
## 同步與異步執行順序
1. `JavaScript`將任務分為同步任務和異步任務,同步任務進入主線中中,異步任務首先到`Event Table`進行回調函數注冊。
2. 當異步任務的**觸發條件滿足**,將回調函數從`Event Table`壓入`Event Queue`中。
3. 主線程里面的同步任務執行完畢,系統會去`Event Queue`中讀取異步的回調函數。
4. 只要主線程空了,就會去`Event Queue`讀取回調函數,這個過程被稱為`Event Loop`。
### 舉個栗子
> * setTimeout(cb, 1000),當1000ms后,就將cb壓入Event Queue。
> * ajax(請求條件, cb),當http請求發送成功后,cb壓入Event Queue。
### EventLoop執行流程
**Event Loop**執行的流程如下:

下面一起來看一個例子,熟悉一下上述流程。
~~~
javascript復制代碼// 下面代碼的打印結果?
// 同步任務 打印 first
console.log("first");
setTimeout(() => {
// 異步任務 壓入Event Table 4ms之后cb壓入Event Queue
console.log("second");
},0)
// 同步任務 打印last
console.log("last");
// 讀取Event Queue 打印second
~~~
### 常見異步任務
* `DOM`事件
* `AJAX`請求
* 定時器`setTimeout`和`setlnterval`
* `ES6`的`Promise`
## 異步任務的優先級
下面繼續來看一個案例:
~~~
javascript復制代碼setTimeout(() => {
console.log(1);
}, 1000)
new Promise(function(resolve){
console.log(2);
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log(3)
});
console.log(4)
~~~
按照上面的學習: 可以很輕松得出案例的打印結果:**2,4,1,3**。
> Promise定義部分為同步任務,回調部分為異步任務
將案例代碼在控制臺運行,最終返回結果卻有些出人意料:

剛看到如此結果,我的第一感覺是,`setTimeout`函數1s觸發太慢導致它加入`Event Queue`的時間晚于`Promise.then`
于是我修改了`setTimeout`的回調時間為0(瀏覽器最小觸發時間為`4ms`),但結果仍為發生改變。
那么也就意味著,`JavaScript`的異步任務是存在優先級的。
## 宏任務和微任務
`JavaScript`除了廣義上將任務劃分為同步任務和異步任務,還對異步任務進行了更精細的劃分。異步任務又進一步分為微任務和宏任務。

> * `history traversal`任務(`h5`當中的歷史操作)
> * `process.nextTick`(`nodejs`中的一個異步操作)
> * `MutationObserver`(`h5`里面增加的,用來監聽`DOM`節點變化的)
宏任務和微任務分別有各自的任務隊列`Event Queue`,即宏任務隊列和微任務隊列。
## Event Loop執行過程
了解到宏任務與微任務過后,我們來學習宏任務與微任務的執行順序。
1. 代碼開始執行,創建一個全局調用棧,`script`作為宏任務執行
2. 執行過程過同步任務立即執行,異步任務根據異步任務類型分別注冊到微任務隊列和宏任務隊列
3. 同步任務執行完畢,查看微任務隊列
* 若存在微任務,將微任務隊列全部執行(包括執行微任務過程中產生的新微任務)
* 若無微任務,查看宏任務隊列,執行第一個宏任務,宏任務執行完畢,查看微任務隊列,重復上述操作,直至宏任務隊列為空
更新一下`Event Loop`的執行順序圖:

## 總結
在上面學習的基礎上,重新分析當前案例:
~~~
javascript復制代碼setTimeout(() => {
console.log(1);
}, 1000)
new Promise(function(resolve){
console.log(2);
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log(3)
});
console.log(4)
~~~
分析過程見下圖:

## 面試題
文章的最后附贈幾道經典面試題,可以測試一下自己對`Event Loop`的掌握程度。
### 題目一
~~~
js復制代碼console.log('script start');
setTimeout(() => {
console.log('time1');
}, 1 * 2000);
Promise.resolve()
.then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
async function foo() {
await bar()
console.log('async1 end')
}
foo()
async function errorFunc () {
try {
await Promise.reject('error!!!')
} catch(e) {
console.log(e)
}
console.log('async1');
return Promise.resolve('async1 success')
}
errorFunc().then(res => console.log(res))
function bar() {
console.log('async2 end')
}
console.log('script end');
~~~
### 題目二
~~~
js復制代碼setTimeout(() => {
console.log(1)
}, 0)
const P = new Promise((resolve, reject) => {
console.log(2)
setTimeout(() => {
resolve()
console.log(3)
}, 0)
})
P.then(() => {
console.log(4)
})
console.log(5)
~~~
### 題目三
~~~
js復制代碼var p1 = new Promise(function(resolve, reject){
resolve("2")
})
setTimeout(function(){
console.log("1")
},10)
p1.then(function(value){
console.log(value)
})
setTimeout(function(){
console.log("3")
},0)
~~~