JavaScript 之徹底理解 EventLoop

前言

Event Loop即事件循環,是瀏覽器或Node解決單線程運行時不會阻塞的一種機制。

在正式學習Event Loop之前,先需要解決幾個問題:

  1. 什麼是同步與異步?

  2. JavaScript是一門單線程語言,那如何實現異步?

  3. 同步任務和異步任務的執行順序如何?

  4. 異步任務是否存在優先級?

同步與異步

計算機領域中的同步與異步和我們現實社會的同步和異步正好相反。現實中的同步,就是同時進行,突出的是 "同",比如看足球比賽的時候喫着零食,兩件事情同時發生;異步就是不同時。但計算機中與現實存在一定差異。

舉個栗子

天氣冷了,早上剛醒來想喝點熱水暖暖身子,但這每天起早貪黑 996,晚上回來太累躺下就睡,沒開水啊,沒法子,只好急急忙忙去燒水。

現在早上太冷了啊,不由得在被窩裏面多躺了一會,收拾的時間緊緊巴巴,不能空等水開,於是我便趁此去洗漱,收拾自己。洗漱完,水開了,喝到暖暖的熱水,舒服啊!

舒服完,開啓新的 996 之日,打工人出發!

燒水和洗漱是在同時間進行的,這就是計算機中的異步

計算機中的同步是連續性的動作,上一步未完成前,下一步會發生堵塞,直至上一步完成後,下一步纔可以繼續執行。例如:只有等水開,才能喝到暖暖的熱水。

單線程卻可以異步?

JavaScript的確是一門單線程語言,但是瀏覽器UI是多線程的,異步任務藉助瀏覽器的線程和JavaScript的執行機制實現。例如,setTimeout就藉助瀏覽器定時器觸發線程的計時功能來實現。

瀏覽器線程

  1. GUI渲染線程
  1. JS引擎線程
  1. 事件觸發線程
  1. 定時器觸發線程
  1. 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 執行的流程如下:

下面一起來看一個例子,熟悉一下上述流程。

// 下面代碼的打印結果?
// 同步任務 打印 first
console.log("first");     
setTimeout(() ={        
// 異步任務 壓入Event Table 4ms之後cb壓入Event Queue
  console.log("second");
},0)
// 同步任務 打印last
console.log("last");     
// 讀取Event Queue 打印second
複製代碼

常見異步任務

異步任務的優先級

下面繼續來看一個案例:

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.nextTicknodejs中的一個異步操作)

  • MutationObserverh5裏面增加的,用來監聽DOM節點變化的)

宏任務和微任務分別有各自的任務隊列Event Queue,即宏任務隊列和微任務隊列。

Event Loop 執行過程

瞭解到宏任務與微任務過後,我們來學習宏任務與微任務的執行順序。

  1. 代碼開始執行,創建一個全局調用棧,script作爲宏任務執行

  2. 執行過程過同步任務立即執行,異步任務根據異步任務類型分別註冊到微任務隊列和宏任務隊列

  3. 同步任務執行完畢,查看微任務隊列

更新一下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