JavaScript 中的事件循環


題意描述: 給出以下代碼的運行結果。


輸入:

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
	console.log('async2');
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');

輸出:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

解題思路:

Alice : 這道題你做錯了呀,來說說你是怎麼想的吧。
Bob: 我知道 JavaScript 中的事件有 微任務宏任務 之分, 每個宏任務內部,總是微任務先執行,然後才執行這個宏任務。
Alice: 你的意思是 每個 宏任務 “綁定” 了一些微任務,在這些微任務執行完之後纔會執行這個宏任務 ?
Bob: 對,但是我不知道 微任務與微任務之間,宏任務與宏任務之間的 執行順序該怎麼確定。
Alice; 這個就說來話長了,我慢慢講給你聽,可以先告訴你,你理解的宏任務和微任務的執行順序是不對的,一個宏任務可能有若干個微任務,在宏任務執行結束之後纔會去執行這些微任務。這道題考察的知識點還是挺多的,只知道微任務和宏任務肯定是答不出來的,你看裏面 又有 async await 又有 setTimeout 又有 Promise,只有對 JavaScript 的 事件循環 掌握了才能答得出來。
Bob: 那你快說呀,我洗耳恭聽。
Alice: 那就先從 任務隊列說起吧。1)JavaScript 中的任務分爲 同步任務異步任務,2) 同步任務都在主線程上執行,形成一個執行棧,3)主線程之外,事件觸發線程管理着一個任務隊列,只要異步任務有了運行結果,就在任務隊列中放置一個事件,4)一旦執行棧中的所有同步任務執行完畢(此時 JavaScript 引擎空閒),系統就會讀取任務隊列,將可運行的異步任務添加到可執行棧中執行。而根據規範,事件循環是通過任務隊列的機制來進行協調的。
Bob: 事件循環是通過任務隊列的機制實現的,這是 how 的問題,還有 why 的問題呢, 爲什麼要有事件循環呢 ?
Alice: 是這樣的,JavaScript 是一個單線程的語言,也就是 從上到下 一行一行的執行代碼。但是這樣就有一個問題,如果遇到了某行 耗時很久的代碼就會一直 阻塞 在那,後面的代碼就無法執行了。比如說,下載一張圖片,發送一個 ajax 請求等等。在等待 圖片下載完成之前,ajax 請求成功之前,其實並沒有事情可做,與其這樣等着,還不如繼續執行後面的代碼。這就有了另一個問題,繼續執行後面的代碼之後,圖片下載完成之後對圖片的操作 該什麼時候執行呢,怎麼實現呢 ?
Bob: 你是說JavaScript 中的 ajax 請求,下載圖片之類的代碼可以看做是 異步任務,而其他順序執行的是 同步任務。異步任務中的代碼並不是立即執行的,而是依靠着事件循環 (有任務隊列實現的)來執行的。
Alice: 是的,維基百科中是這樣描述事件循環的: “Event Loop是一個程序結構,用於等待和發送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)”在這裏插入圖片描述
上圖主線程的綠色部分,表示運行時間,橙色部分表示空閒時間。每當遇到I/O的時候,主線程就讓Event Loop線程去通知相應的I/O程序,然後接着往後運行,所以不存在等待時間。等到I/O程序完成操作,Event Loop線程再把結果返回主線程。主線程就調用事先設定的回調函數,完成整個任務。
在這裏插入圖片描述
虛線框住的是 JS 引擎,然後 stack 就是執行棧,執行棧中的代碼在運行過程中產生的 異步操作會在 操作完成後在 任務隊列中註冊回調函數。當執行棧爲空的時候,會從任務隊列中 取出隊頭的回調函數 放入執行棧中執行,如此循環往復,是爲事件循環。
在這裏插入圖片描述
用流程圖的方式描述事件循環。

Bob: 看完這幾張圖,大體上能夠理解事件循環了。事件循環是 JavaScript 中處理異步代碼的方式,正是有了 事件循環 JavaScript 纔是 非阻塞(Non-blocking)的。事件循環的具體實現就是,當主線程遇到異步代碼的時候,將異步代碼交給 消息線程 去處理,消息線程會在異步操作結束後,向事件隊列(任務隊列)中添加回調函數,當主線程的執行棧爲空的是,再從事件隊列中取出隊頭的回調函數放入 執行棧中執行。而事件循環所指的就是這個,循環往復的過程。
Alice: 😉你總結的很不錯嘛,What, Why, How 都有了。
Bob: 然後呢,微任務和宏任務在任務隊列中是怎麼樣的 ?微任務和微任務之間,宏任務和宏任務之間,微任務和宏任務之間的關係呢 ?
Alice: 還有一張圖很不錯,先看這個。
在這裏插入圖片描述
圖裏面 Task Queue 可以看出是任務隊列(宏任務)(事件隊列)Microtask Queue 就是微觀隊列,看到了吧,一個宏任務對應着一個微任務隊列。
在這裏插入圖片描述
這張圖也是這個意思,一個宏任務對應着若干個微任務組成的微任務隊列。
Bob: 額,微任務之間的執行順序是按照 微任務隊列中的順序 從隊頭到隊尾 ? 宏任務之間的執行順序就是按照 任務隊列 中的順序 從 隊頭到隊尾是嗎 ?
Alice 是的,宏任務主要有:script(整體代碼)、setTimeoutsetIntervalI/OUI交互事件postMessageMessageChannelsetImmediate(Node.js 環境)
微任務主要包含:Promise.thenMutaionObserver(與DOM有關)、process.nextTick(Node.js 環境)。
然後運行的機制大概是這樣的:

  • 執行一個宏任務(棧中沒有就從事件隊列中獲取)
  • 執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中
  • 宏任務執行完畢後,立即執行當前微任務隊列中的所有微任務(依次執行)
  • 當前宏任務執行完畢,開始檢查渲染,然後GUI線程接管渲染
  • 渲染完畢後,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)
    在這裏插入圖片描述

Bob: 那 await 呢,await 後面的代碼算宏任務還是微任務呀 ?
Alice: 從字面意思上看await就是等待,await 等待的是一個表達式,這個表達式的返回值可以是一個promise對象也可以是其他值。很多人以爲await會一直等待之後的表達式執行完之後纔會繼續執行後面的代碼,實際上await是一個讓出線程的標誌。await後面的表達式會先執行一遍,將await後面的代碼加入到microtask中,然後就會跳出整個async函數來執行後面的代碼。
Bob:await 這樣子好像還是回調函數那一套呀,只不過是同步代碼的寫法。
Alice: 對啊,現在我們就可以 重新分析一下上面那道題了。

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
	console.log('async2');
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');

在這一段代碼中首先定義了兩個 async 函數 async1, async2, 然後執行了 console.log('script start'),所以控制檯肯定是 {script start}。然後接着往下走,遇到一個 setTimeout ,放到任務隊列中【setTimeout1, 】。然後執行了 async1() async1 是一個函數,執行第一行 輸出 'async1 start'此時控制檯是{script start, async1 start, } 然後第二行 有 await ,await 會把 async2 執行了,輸出async2,此時控制檯是{script start, async1 start, async2} 然後把 console.log('async1 end');放入微任務隊列【async1 end,】。這樣 async1 就執行完了。接着往下後, Promise 中的代碼立即執行,輸出 promise1, 此時控制檯是{script start, async1 start, async2, promise1}。然後 then 裏面的代碼被放到 微任務隊列中 【async1 end, promise2】,然後執行最後一句,控制檯中變成 {script start, async1 start, async2, promise1,script end,}。到現在 任務隊列中有 【setTimeout】微任務隊列中有 【async1 end, promise2】 。然後依次執行微任務對應的代碼,控制檯中的輸出應該變成{script start, async1 start, async2, promise1,script end, async1 end, promise2}。然後這個宏任務就結束了,開始下一個宏任務,輸出 setTimeout 就結束了。最終的結果就是{script start, async1 start, async2, promise1,script end, async1 end, promise2, setTimeout}
Bob: 醍醐灌頂,恍然大悟呀。
Alice: JavaScript 中的事件循環大概就是這樣了,不過上面的描述中還有很多不規範的地方,以後再來完善吧。
Bob:I’ll be the roundabout…
在這裏插入圖片描述


易錯點:

script start
async1 start
async2          // 誤認爲await 會阻塞 後面的代碼
async1 end
setTimeOut      //  不知道script 本身就是一個宏任務
promise1     
promise2        //  不知道 then 中的代碼會被添加到微任務
script end

總結:

舉一反三,再來點訓練吧。

Promise.resolve().then(function promise1 () {
       console.log('promise1');
    })
setTimeout(function setTimeout1 (){
    console.log('setTimeout1')
    Promise.resolve().then(function  promise2 () {
       console.log('promise2');
    })
}, 0)

setTimeout(function setTimeout2 (){
   console.log('setTimeout2')
}, 0)
// 執行結果 promise1 setTimeout1 promise2 setTimeout2
setTimeout(function () {
    console.log(1);
});

new Promise(function(resolve,reject){
    console.log(5)
    resolve(2)
}).then(function(val){
    console.log(val);
})
// 執行結果 5 2 1
console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
// 執行結果 1768 2435 9 11 10 12
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    //async2做出如下更改:
    new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
    });
}
console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();

new Promise(function(resolve) {
    console.log('promise3');
    resolve();
}).then(function() {
    console.log('promise4');
});

console.log('script end');
// 執行結果 
/*
script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout
*/
async function async1() {
    console.log('async1 start');
    await async2();
    //更改如下:
    setTimeout(function() {
        console.log('setTimeout1')
    },0)
}
async function async2() {
    //更改如下:
	setTimeout(function() {
		console.log('setTimeout2')
	},0)
}
console.log('script start');

setTimeout(function() {
    console.log('setTimeout3');
}, 0)
async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');
/*執行結果
script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1
*/
async function a1 () {
    console.log('a1 start')
    await a2()
    console.log('a1 end')
}
async function a2 () {
    console.log('a2')
}

console.log('script start')

setTimeout(() => {
    console.log('setTimeout')
}, 0)

Promise.resolve().then(() => {
    console.log('promise1')
})

a1()

let promise2 = new Promise((resolve) => {
    resolve('promise2.then')
    console.log('promise2')
})

promise2.then((res) => {
    console.log(res)
    Promise.resolve().then(() => {
        console.log('promise3')
    })
})
console.log('script end')
/*
運行結果
script start
a1 start
a2
promise2
script end
promise1
a1 end
promise2.then
promise3
setTimeout
*/

參考文獻:


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章