JavaScript 之徹底理解 EventLoop
前言
Event Loop
即事件循環,是瀏覽器或Node
解決單線程運行時不會阻塞的一種機制。
在正式學習Event Loop
之前,先需要解決幾個問題:
-
什麼是同步與異步?
-
JavaScript
是一門單線程語言,那如何實現異步? -
同步任務和異步任務的執行順序如何?
-
異步任務是否存在優先級?
同步與異步
計算機領域中的同步與異步和我們現實社會的同步和異步正好相反。現實中的同步,就是同時進行,突出的是 "同",比如看足球比賽的時候喫着零食,兩件事情同時發生;異步就是不同時。但計算機中與現實存在一定差異。
舉個栗子
天氣冷了,早上剛醒來想喝點熱水暖暖身子,但這每天起早貪黑 996,晚上回來太累躺下就睡,沒開水啊,沒法子,只好急急忙忙去燒水。
現在早上太冷了啊,不由得在被窩裏面多躺了一會,收拾的時間緊緊巴巴,不能空等水開,於是我便趁此去洗漱,收拾自己。洗漱完,水開了,喝到暖暖的熱水,舒服啊!
舒服完,開啓新的 996 之日,打工人出發!
燒水和洗漱是在同時間進行的,這就是計算機中的異步。
計算機中的同步是連續性的動作,上一步未完成前,下一步會發生堵塞,直至上一步完成後,下一步纔可以繼續執行。例如:只有等水開,才能喝到暖暖的熱水。
單線程卻可以異步?
JavaScript
的確是一門單線程語言,但是瀏覽器UI
是多線程的,異步任務藉助瀏覽器的線程和JavaScript
的執行機制實現。例如,setTimeout
就藉助瀏覽器定時器觸發線程的計時功能來實現。
瀏覽器線程
GUI
渲染線程
-
繪製頁面,解析 HTML、CSS,構建 DOM 樹等
-
頁面的重繪和重排
-
與 JS 引擎互斥 (JS 引擎阻塞頁面刷新)
JS
引擎線程
-
js 腳本代碼執行
-
負責執行準備好的事件,例如定時器計時結束或異步請求成功且正確返回
-
與 GUI 渲染線程互斥
- 事件觸發線程
-
當對應的事件滿足觸發條件,將事件添加到 js 的任務隊列末尾
-
多個事件加入任務隊列需要排隊等待
- 定時器觸發線程
-
負責執行異步的定時器類事件:setTimeout、setInterval 等
-
瀏覽器定時計時由該線程完成,計時完畢後將事件添加至任務隊列隊尾
HTTP
請求線程
-
負責異步請求
-
當監聽到異步請求狀態變更時,如果存在回調函數,該線程會將回調函數加入到任務隊列隊尾
同步與異步執行順序
-
JavaScript
將任務分爲同步任務和異步任務,同步任務進入主線中中,異步任務首先到Event Table
進行回調函數註冊。 -
當異步任務的觸發條件滿足,將回調函數從
Event Table
壓入Event Queue
中。 -
主線程裏面的同步任務執行完畢,系統會去
Event Queue
中讀取異步的回調函數。 -
只要主線程空了,就會去
Event Queue
讀取回調函數,這個過程被稱爲Event Loop
。
舉個栗子
setTimeout(cb, 1000),當 1000ms 後,就將 cb 壓入 Event Queue。
ajax(請求條件, cb),當 http 請求發送成功後,cb 壓入 Event Queue。
EventLoop 執行流程
Event Loop 執行的流程如下:
下面一起來看一個例子,熟悉一下上述流程。
// 下面代碼的打印結果?
// 同步任務 打印 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
異步任務的優先級
下面繼續來看一個案例:
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 執行過程
瞭解到宏任務與微任務過後,我們來學習宏任務與微任務的執行順序。
-
代碼開始執行,創建一個全局調用棧,
script
作爲宏任務執行 -
執行過程過同步任務立即執行,異步任務根據異步任務類型分別註冊到微任務隊列和宏任務隊列
-
同步任務執行完畢,查看微任務隊列
-
若存在微任務,將微任務隊列全部執行 (包括執行微任務過程中產生的新微任務)
-
若無微任務,查看宏任務隊列,執行第一個宏任務,宏任務執行完畢,查看微任務隊列,重複上述操作,直至宏任務隊列爲空
更新一下Event Loop
的執行順序圖:
總結
在上面學習的基礎上,重新分析當前案例:
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
的掌握程度。
題目一
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');
複製代碼
題目二
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)
複製代碼
題目三
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)
複製代碼
關於本文
https://juejin.cn/post/7020328988715270157
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/a1WZTFpWxr5x9TAy8iYMHg