Event Loop 原來是這麼回事
前沿
從前有座山,山裡有座廟,廟裡有個小和尚在講故事、講什麼呢?講的是:
從前有座山,山裡有座廟,廟裡有個小和尚在講故事、講什麼呢?講的是:
從前有座山,山裡有座廟,廟裡有個小和尚在講故事、講什麼呢?講的是:
...
請看一個小故事
以前有一個餐廳,這個餐廳有一個老闆和一個廚師,自己創業的,剛開始起步階段,沒有資金請員工,所以自己來當老闆兼服務員。
來這家餐廳的顧客有這四種類型
- 超級VIP客戶
- VIP客戶
- 普通的客戶
- 賴賬的客戶
作為VIP顧客,肯定得有VIP特權。
- 優先上菜
- 同等VIP,先點菜的先上菜
所以這個店的上菜順序跟身份和點菜的順序有關,
超級VIP客戶 > VIP客戶 > 普通客戶 > 賴賬的客戶
一天、老闆開始營業後,陸陸續續的來了一些人進來點餐吃飯。
-
第一個進來的是普通的客戶,點了一道回鍋肉
-
第二個進來的是充了錢的VIP客戶,點了一道小龍蝦
-
第三個進來的是賴賬的客戶,點了很多菜
-
第四個進來的是一個超級VIP客戶,點了一道酸菜魚
由於這個店只有一個人,所以老闆招待好他們點完餐之後就去炒菜了。根據上面提到的順序,所以會先炒超級VIP客戶點的菜、然後到VIP客戶點的菜、然後到普通客戶點的菜、最後到賴賬的客戶點的菜
讓我們用虛擬碼看看如何實現這個邏輯
我們定義了四個function
- superVipOrder(name, dish) 用來表示超級VIP使用者下單點菜
- vipOrder(name, dish) 用來表示VIP使用者下單點菜
- order(name, dish) 用來表示普通使用者下單點菜
- groupOrder(name, dish) 用來表示賴賬的客戶下單點菜
根據上面提到的上菜規則,
超級VIP客戶 > VIP客戶 > 普通客戶 > 賴賬的客戶
實際的上菜順序我們可以知道是
那麼問題來了,那些function都是什麼呢?
其實很簡單,這些function
// 超級VIP客戶
// 微任務,將回調加入到 執行佇列,優先執行
function superVipOrder(name, dish, order) {
process.nextTick(() => {
console.log(red(`superVip顧客 ${name} 點了 ${dish} 已經上了`));
});
}
複製程式碼
// VIP客戶
// 微任務,將回調加入到 執行佇列,優先執行,優先順序比process.nextTick低
function vipOrder(name, dish, order) {
new Promise((resolve, reject) => {
resolve(true);
}).then(() => {
console.log(blue(`vip顧客 ${name} 點了 ${dish} 已經上了`));
});
}
複製程式碼
// 普通客戶下單
// 巨集任務,將回調加入到 巨集任務的執行佇列中
function order(name, dish, order) {
setTimeout(() => {
console.log(yellow(`普通顧客 ${name} 點了 ${dish} 已經上了`));
}, 0);
}
複製程式碼
// 賴賬的客戶下單
function groupOrder(name, dish, order) {
setImmediate(() => {
console.log(green(`賴賬的顧客 ${name} 點了 ${dish} 已經上了`));
});
}
複製程式碼
我們可以暫且先把 process.nextTick
認為是超級vip使用者,優先順序最高、
原生Promise
認為是vip使用者,執行優先順序高
setTimeout
認為是普通使用者,執行優先順序一般
setImmediate
認為是賴賬的顧客,執行優先順序低
還原虛擬碼
我們將虛擬碼還原成這些非同步函式,這會讓我們看的更加直觀、親切一些
根據上面故事提到的優先順序規則,我們知道輸出的結果是這樣的
為什麼會是這樣的結果呢?下面就來講講JavaScript中的Event Loop
Event Loop
1. JavaScript的事件迴圈
我們知道 JavaScript
是單執行緒的,就像上面故事的老闆,他得去服務員去招待客人點菜,並將選單給廚師,廚師炒好後再給到他去上菜。如果老闆不請個廚師,自己來炒菜的話,那麼在炒菜時就沒辦法接待客人,客人就會等待點菜。等著等著就會暴露出服務態度不行的問題。所以說,得有廚師專門處理炒菜的任務
所以在js中,任務分為同步任務和非同步任務,
- 同步任務 -> 服務員去接待客人點菜
- 非同步任務 -> 廚師炒菜、非同步回撥函式相當於 服務員去上菜
JS的事件迴圈如圖所示,
- 在執行主執行緒的任務時,如果有非同步任務,會進入到Event Table並註冊回撥函式,當指定的事情完成後,會將這個回撥函式放到 callback queue 中
- 在主執行緒執行完畢之後,會去讀取 callback queue中的回撥函式,進入主執行緒執行
- 不斷的重複這個過程,也就是常說的Event Loop(事件迴圈)了
2. 非同步任務
非同步任務又分為巨集任務跟微任務、他們之間的區別主要是執行順序的不同。
在js中,微任務有
-
原生的Promise -> 其實就是我們上面提到的VIP使用者,
-
process.nextTick -> 其實就是我們上面提到的超級VIP使用者,
process.nextTick的執行優先順序高於Promise的
巨集任務
- 整體程式碼 script
- setTimeout -> 其實就是我們上面提到的普通使用者,
- setImmediate -> 其實就是我們上面提到的群體使用者,
setTimeout的執行優先順序高於 setImmediate 的
巨集任務與微任務的執行過程
在一次事件迴圈中,JS會首先執行 整體程式碼 script,執行完後會去判斷微任務佇列中是否有微任務,如果有,將它們逐一執行完後在一次執行巨集任務。如此流程
測試
下面我們來看一段程式碼是否瞭解了這個流程
<script>
setTimeout(() => {
console.log('a');
new Promise( res => {
res()
}).then( () => {
console.log('c');
})
process.nextTick(() => {
console.log('h');
})
}, 0)
console.log('b');
process.nextTick( () => {
console.log('d');
process.nextTick(() => {
console.log('e');
process.nextTick(() => {
console.log('f');
})
})
})
setImmediate( () => {
console.log('g');
})
</script>
複製程式碼
執行結果為:b d e f a h c g
讓我們來分析一下這段程式碼的執行流程
-
首頁執行第一個巨集任務 整段
script
標籤程式碼,遇到第一個setTimeout
,將其回撥函式加入到巨集任務佇列中, -
輸出
console.log('b')
-
遇到process.nextTick,將其回撥函式加入到微任務
-
遇到setImmediate 將其回撥函式加入到巨集任務佇列中
巨集任務Event Queue | 微任務Event Queue |
---|---|
setTimeout | process.nextTick |
setImmediate |
- 當第一個巨集任務執行完後,就會去判斷是否還有微任務,剛好有一個 微任務,執行process.nextTick的回撥,輸出
console.log('d')
,然後又遇到了一個process.nextTick,又將其放入到微任務佇列 - 繼續將微任務佇列中的回撥函式取出,繼續執行,輸出
console.log('e')
,然後又遇到了一個process.nextTick,又將其放入到微任務佇列 - 繼續將微任務佇列中的回撥函式取出,繼續執行,輸出
console.log('f')
,然後又遇到了一個process.nextTick,又將其放入到微任務佇列
巨集任務Event Queue | 微任務Event Queue |
---|---|
setTimeout | |
setImmediate |
- 當微任務佇列為空後,開始新的巨集任務,取出第一個巨集任務佇列的函式,
setTimeout
,執行console.log('a')
,然後遇到Promise
,process.nextTick
將其回撥加入到微任務佇列。執行完後
巨集任務Event Queue | 微任務Event Queue |
---|---|
setImmediate | promise.then |
- | process.nextTick |
- 繼續判斷微任務佇列是否有回撥函式可執行,由於
process.nextTick
的執行優先順序大於promise
,所以會先執行process.nextTick
的回撥,輸出console.log('h');
、如果有多個process.nextTick
的回撥,會將process.nextTick
的所有回撥執行完成後才會去執行其它微任務的回撥。 當nextTick所有的回撥執行完後,執行promise
的回撥,輸出console.log('c');
,直到promise的回撥佇列執行完後,又會去判斷是否還有微任務。
巨集任務Event Queue | 微任務Event Queue |
---|---|
setImmediate |
- 微任務執行完後,開始執行新的巨集任務,執行
setImmediate
的回撥,輸出console.log('g');
3. setImmediate
這裡為什麼要把 setImmediate
單獨拿出來說呢,因為它屬於巨集任務的範疇,但又有點不一樣的地方。
先看一段程式碼
按照我們上面的分析邏輯,我們會認為這段程式碼的輸出結果應該是a b c d
。 如果我們把使用Node 0.10.x的版本去執行這段程式碼,結果確實是輸出a b c d
然而,在Node 大於 4.x 的版本後,在執行setImmediate
的,會使用while迴圈,把所有的immediate回撥取出來依次進行處理。
最後看一段程式碼看看自己是否真的掌握了
如果還沒有掌握,歡迎評論區吐槽
<script>
console.log("start");
process.nextTick(() => {
console.log("a");
setImmediate(() => {
console.log("d");
});
new Promise(res => res()).then(() => {
console.log("e");
process.nextTick(() => {
console.log("f");
});
new Promise(r => {
r()
})
.then(() => {
console.log("g");
});
setTimeout(() => {
console.log("h");
});
});
});
setImmediate(() => {
console.log("b");
process.nextTick(() => {
console.log("c");
});
new Promise(res => res()).then(() => {
console.log("i");
});
});
console.log("end");
</script>
複製程式碼
輸出的結果為: start end a e g f h b d c i
簡單分析一下程式碼:
- 第一輪事件迴圈開始,執行
script
程式碼,輸出start
end
,將process.nextTick
的回撥加入微任務佇列中,將setImmediate
的回撥加入到巨集任務的佇列中 - 執行微任務佇列中的
process.nextTick
的回撥,輸出a
、將setImmediate
的回撥加入到巨集任務的佇列中,遇到promise
、將回調加入到微任務佇列中。
巨集任務 | 微任務 |
---|---|
setImmediate | promise.then |
setImmediate | - |
- 繼續執行微任務佇列中的回撥,取出
promise.then
並執行,輸出e
,將process.nextTick
的回撥放入到微任務中,遇到promise
、將回調加入到微任務佇列中。 - 判斷當前promise的回撥佇列是否還有回撥函式沒執行,如果有,將繼續執行,取出剛剛放入的promise的回撥,輸出
g
,當Promise回撥佇列執行完後,繼續判斷當前是否還有微任務。 - 取出
process.nextTick
的回撥並執行,輸出g
巨集任務 | 微任務 |
---|---|
setImmediate | - |
setImmediate | - |
setTimeout | - |
- 當前微任務佇列為空後,開始執行巨集任務,因為
setTimeout
的優先順序大於setImmediate
,所以先取出setTimeout
的回撥並執行,輸出h
- 當前微任務佇列還是為空,開始執行巨集任務,取出所有
setImmediate
的回撥函式,並執行,輸出b d
,將process.next
與promise
的回撥放入到微任務佇列中。 - 取出微任務佇列中的回撥函式,並執行,輸出
c i
總結
Event Loop 作為面試的高頻題,靜下心來認真的分析一下,其實不難理解。
歡迎關注
歡迎關注公眾號“碼上開發”,每天分享最新技術資訊