問題描述
在看事件循環相關視頻的時候發現其中有一個例子不理解,查了資料才明白其中的緣由,遂以志之。問題是這樣的:
<button id="button">button</button>
button.addEventListener("click", () => {
Promise.resolve().then(() => console.log("Microtask 1"));
console.log("Listener 1");
});
button.addEventListener("click", () => {
Promise.resolve().then(() => console.log("Microtask 2"));
console.log("Listener 2");
});
要問的是:
- 你手動點擊button,打印的log順序是啥?
-
如果改成用js調用lick的方式,那麼輸出順序又是啥?
// 在上面的js代碼最後一行新增 button.click();
問題解析
首先,公佈一下答案。
第一種情況輸出是:
Listener 1
Microtask 1
Listener 2
Microtask 2
第二種情況的輸出是:
Listener 1
Listener 2
Microtask 1
Microtask 2
先回顧一下時間循環中的部分知識點:
在事件循環中,點擊事件的回調屬於Macrotask,Promise.then的回調屬於Microtask,先執行一個Macrotask,然後等調用棧為空之後就會執行所有Microtask。
結合上述知識,情況1的答案是完全符合這個説法的。
但是情況2就不能理解了,為何連着執行了兩個Macrotask,然後才執行的Microtask,難道這個這個邏輯有特殊情況?非也!
問題的核心在於手動點擊的方式回調是異步的,而button.click()這種調用方式它是同步的,沒錯,它是同步調用的。
和經由瀏覽器觸發,並通過事件循環異步調用事件處理程序的“原生”事件不同,dispatchEvent() 會同步調用事件處理函數。在 dispatchEvent() 返回之前,所有監聽該事件的事件處理程序將在代碼繼續前執行並返回。
來源文檔MDN:EventTarget.dispatchEvent()
明白這一點之後再看情況2就很好理解了,就相當於:
function fn1() {
Promise.resolve().then(() => console.log("Microtask 1"));
console.log("Listener 1");
});
function fn2 {
Promise.resolve().then(() => console.log("Microtask 2"));
console.log("Listener 2");
});
// button.click();
fn1();
fn2();
最後
這個題目出現在一個與事件循環相關的視頻中,於是有不少文章在解釋情況2的時候都會解釋説此時調用棧不為空,所以不能執行Microtask,但卻沒有解釋為啥不為空,以及調用棧裏有啥。我看了解釋完全摸不着頭腦,最後在瞭解了dispatchEvent同步調用的知識點後才恍然大悟,這根本就是兩碼事嘛!吃了沒文化的虧。