導(dǎo)論
先看題目:
question:試著寫出下面程序的輸出結(jié)果:
console.log(1);
setTimeout(() => {
console.log(2);
new Promise((resolve) => {
console.log(3);
resolve();
}).then(res => {
console.log(4);
})
console.log(5);
}, 0);
new Promise((resolve) => {
console.log(6);
resolve();
}).then(res => {
console.log(7);
})
setTimeout(() => {
console.log(8);
new Promise((resolve) => {
console.log(9);
resolve();
}).then(res => {
console.log(10);
}, 0)
});
console.log(11);
answer:1 -> 6 -> 11 -> 7 -> 2 -> 3 -> 5 -> 4 -> 8 -> 9 -> 10
宏任務(wù)與微任務(wù)
在javascript事件循環(huán)中,==異步**任務(wù)是通過隊列(queue)來存儲的。流程如下:

而異步任務(wù)一共分為兩種:微任務(wù)與宏任務(wù)。它們的執(zhí)行順序如下:

需要注意的是,微任務(wù)與宏任務(wù)是先注冊,再執(zhí)行。而不是讀取到就立即執(zhí)行。
常見的宏任務(wù)(按優(yōu)先級排列):整體代碼script > setImmediate > setTimeout/setInterval。
常見的微任務(wù)(按優(yōu)先級排列):process.nextTick(Nodejs中的內(nèi)容) > 原生Promise > MutationObserver。
回到剛才的那個題目:
// 主代碼塊
console.log(1);
// 注冊了一個宏任務(wù)
setTimeout(() => {
// ...
}, 0);
// {↓標(biāo)記↓}
new Promise((resolve) => {
// 立即執(zhí)行部分,其實(shí)是同步任務(wù)
console.log(6);
resolve();
}).then(res => {
// 這才是微任務(wù)
console.log(7);
})
// 注冊了一個宏任務(wù)
setTimeout(() => {
// ...
});
// 主代碼塊
console.log(11);
所以一開始先執(zhí)行主代碼塊,輸出1、6、11,請注意,Promise構(gòu)造函數(shù)的參數(shù)中的代碼是同步任務(wù),與主代碼塊同步進(jìn)行。
執(zhí)行完主代碼塊后,會尋找微任務(wù)隊列中的微任務(wù)并執(zhí)行,此時的微任務(wù)只有標(biāo)記的Promise.then()(其他Promsie所在的代碼塊并未被執(zhí)行,因此尚未被注冊),輸出7。
微任務(wù)執(zhí)行完后,尋找下一個宏任務(wù) — 第一個SetTimeOut。
setTimeout(() => {
console.log(2);
new Promise((resolve) => {
console.log(3);
resolve();
}).then(res => {
console.log(4);
})
console.log(5);
}, 0);
同理可得:將輸出2、3、5,并注冊一個微任務(wù)(Promise.then())。在執(zhí)行完后執(zhí)行微任務(wù),輸出4。
然后再是最后一個宏任務(wù):
setTimeout(() => {
console.log(8);
new Promise((resolve) => {
console.log(9);
resolve();
}).then(res => {
console.log(10);
}, 0)
});
8、9、10,沒什么問題了吧,別問,問就同理可得。
西江月·證明
即得易見平凡,仿照上例顯然。留作習(xí)題答案略,讀者自證不難。
反之亦然同理,推論自然成立,略去過程Q.E.D ,由上可知證畢。
其實(shí)這個題還差了點(diǎn)意思,換做我就這樣出:
console.log(1);
setTimeout(() => {
console.log(2);
new Promise((resolve) => {
console.log(3);
resolve();
}).then(res => {
console.log(4);
})
console.log(5);
}, 0);
new Promise((resolve) => {
console.log(6);
resolve();
}).then(res => {
console.log(7)
setTimeout(() => {
console.log(8);
});
})
console.log(9);
猜一下答案輸出順序是什么?
...
...
...
...
...
...
...
...
...
答案是 1 -> 6 -> 9 -> 7 -> 2 -> 3 -> 5 -> 4 -> 8。
拓展
常見的宏任務(wù)除了上面提到的那些之外,還有一個不曾提到但非常常見的:IO(輸入輸出流)。你可以簡單理解為事件監(jiān)聽(最常見的表現(xiàn)就是事件監(jiān)聽,不過不僅僅包括事件監(jiān)聽)。
上代碼:
/* css */
div {
border: 1px solid black;
}
#outer {
padding: 25px;
width: 50px;
background-color: aqua;
}
#inner {
width: 50px;
height: 50px;
background-color: green;
}
<!--html-->
<div id="outer">
<div id="inner"></div>
</div>
// js
let $outer = document.getElementById('outer');
let $inner = document.getElementById('inner');
function handler() {
console.log('click') // 直接輸出
// 注冊微任務(wù)
Promise.resolve().then(_ => console.log('promise'))
// 注冊宏任務(wù)
setTimeout(_ => console.log('timeout'))
// 注冊宏任務(wù)
requestAnimationFrame(_ => console.log('animationFrame'));
// DOM屬性修改,觸發(fā)微任務(wù)
$outer.setAttribute('data-random', Math.random());
}
// 微任務(wù)
new MutationObserver(_ => {
console.log('observer')
}).observe($outer, {
attributes: true
})
$inner.addEventListener('click', handler);
$outer.addEventListener('click', handler);

點(diǎn)擊div#inner,輸出結(jié)果: 'click' -> 'promise' -> 'observer' -> 'click' -> 'promise' -> 'observer' -> 'animationFrame' -> 'animationFrame' -> 'timeout' -> 'timeout'。
讓我們來捋一下:
點(diǎn)擊時通過事件冒泡觸發(fā)了宏任務(wù)$inner.click(),輸出'click'。該任務(wù)觸發(fā)了冒泡事件并注冊了宏任務(wù)$outer.click()。并在事件處理函數(shù)handler中注冊了兩個宏任務(wù)(requestAnimationFrame和setTimeout)和一個微任務(wù)(Promise),并觸發(fā)了一個微任務(wù)MutationObserver。
該宏任務(wù)執(zhí)行完后,微任務(wù)隊列中有兩個微任務(wù),按優(yōu)先級先執(zhí)行Promise,所以依次輸出'promise','observer'。
下一個宏任務(wù),即$outer.click()開始執(zhí)行,重復(fù)了$inner.click()的事件,區(qū)別是它沒有冒泡并注冊新任務(wù)了。依次輸出'click','promise','observer'。
至此,只剩下幾個宏任務(wù)對線了。
值得注意的是,因為requestAnimationFrame會觸發(fā)頁面重繪(不是優(yōu)先級哦),進(jìn)而會導(dǎo)致setTimeout重置,所以前者會比后者先輸出。
后記
祝你學(xué)習(xí)愉快。