JS事件循環(huán)之宏任務(wù)和微任務(wù)
眾所周知臀栈,JS 是一門單線程語言月腋,可是瀏覽器又能很好的處理異步請求妄迁,那么到底是為什么呢疗韵?
JS 的執(zhí)行環(huán)境一般是瀏覽器和 Node.js兑障,兩者稍有不同,這里只討論瀏覽器環(huán)境下的情況蕉汪。
JS 執(zhí)行過程中會產(chǎn)生兩種任務(wù)流译,分別是:同步任務(wù)和異步任務(wù)。
- 同步任務(wù):比如聲明語句者疤、for福澡、賦值等,讀取后依據(jù)從上到下從左到右驹马,立即執(zhí)行革砸。
- 異步任務(wù):比如 ajax 網(wǎng)絡(luò)請求,setTimeout 定時函數(shù)等都屬于異步任務(wù)糯累。異步任務(wù)會通過任務(wù)隊列(Event Queue)的機(jī)制(先進(jìn)先出的機(jī)制)來進(jìn)行協(xié)調(diào)算利。
任務(wù)隊列(Event Queue)
任務(wù)隊列中的任務(wù)也分為兩種,分別是:宏任務(wù)(Macro-take)和微任務(wù)(Micro-take)
- 宏任務(wù)主要包括:scrip(JS 整體代碼)泳姐、setTimeout效拭、setInterval、setImmediate、I/O缎患、UI 交互
- 微任務(wù)主要包括:Promise(重點關(guān)注)慕的、process.nextTick(Node.js)、MutaionObserver
任務(wù)隊列的執(zhí)行過程是:先執(zhí)行一個宏任務(wù)挤渔,執(zhí)行過程中如果產(chǎn)出新的宏/微任務(wù)肮街,就將他們推入相應(yīng)的任務(wù)隊列,之后在執(zhí)行一隊微任務(wù)蚂蕴,之后再執(zhí)行宏任務(wù)低散,如此循環(huán)俯邓。以上不斷重復(fù)的過程就叫做 Event Loop(事件循環(huán))骡楼。
每一次的循環(huán)操作被稱為tick。
理解微任務(wù)和宏任務(wù)的執(zhí)行執(zhí)行過程
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
Promise.resolve()
.then(function () {
console.log("promise1");
})
.then(function () {
console.log("promise2");
});
console.log("script end");
按照上面的內(nèi)容稽鞭,分析執(zhí)行步驟:
- 宏任務(wù):執(zhí)行整體代碼(相當(dāng)于
<script>
中的代碼):- 輸出:
script start
- 遇到 setTimeout鸟整,加入宏任務(wù)隊列,當(dāng)前宏任務(wù)隊列(setTimeout)
- 遇到 promise朦蕴,加入微任務(wù)篮条,當(dāng)前微任務(wù)隊列(promise1)
- 輸出:
script end
- 輸出:
- 微任務(wù):執(zhí)行微任務(wù)隊列(promise1)
- 輸出:
promise1
,then 之后產(chǎn)生一個微任務(wù)吩抓,加入微任務(wù)隊列涉茧,當(dāng)前微任務(wù)隊列(promise2) - 執(zhí)行 then,輸出
promise2
- 輸出:
- 執(zhí)行渲染操作疹娶,更新界面(敲黑板劃重點)伴栓。
- 宏任務(wù):執(zhí)行 setTimeout
- 輸出:
setTimeout
- 輸出:
Promise 的執(zhí)行
new Promise(..)
中的代碼,也是同步代碼雨饺,會立即執(zhí)行钳垮。只有then
之后的代碼,才是異步執(zhí)行的代碼额港,是一個微任務(wù)饺窿。
console.log("script start");
setTimeout(function () {
console.log("timeout1");
}, 10);
new Promise((resolve) => {
console.log("promise1");
resolve();
setTimeout(() => console.log("timeout2"), 10);
}).then(function () {
console.log("then1");
});
console.log("script end");
步驟解析:
- 當(dāng)前任務(wù)隊列:微任務(wù): [], 宏任務(wù):[
<script>
]
- 宏任務(wù):
- 輸出:
script start
- 遇到 timeout1,加入宏任務(wù)
- 遇到 Promise移斩,輸出
promise1
肚医,直接 resolve,將 then 加入微任務(wù)向瓷,遇到 timeout2忍宋,加入宏任務(wù)。 - 輸出
script end
- 宏任務(wù)第一個執(zhí)行結(jié)束
- 輸出:
- 當(dāng)前任務(wù)隊列:微任務(wù)[then1]风罩,宏任務(wù)[timeou1, timeout2]
- 微任務(wù):
- 執(zhí)行 then1糠排,輸出
then1
- 微任務(wù)隊列清空
- 執(zhí)行 then1糠排,輸出
- 當(dāng)前任務(wù)隊列:微任務(wù)[],宏任務(wù)[timeou1, timeout2]
- 宏任務(wù):
- 輸出
timeout1
- 輸出
timeout2
- 輸出
- 當(dāng)前任務(wù)隊列:微任務(wù)[]超升,宏任務(wù)[timeou2]
- 微任務(wù):
- 為空跳過
- 當(dāng)前任務(wù)隊列:微任務(wù)[]入宦,宏任務(wù)[timeou2]
- 宏任務(wù):
- 輸出
timeout2
- 輸出
async/await 的執(zhí)行
async 和 await 其實就是 Generator 和 Promise 的語法糖哺徊。
async 函數(shù)和普通 函數(shù)沒有什么不同,他只是表示這個函數(shù)里有異步操作的方法乾闰,并返回一個 Promise 對象
翻譯過來其實就是:
// async/await 寫法
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
// Promise 寫法
async function async1() {
console.log("async1 start");
Promise.resolve(async2()).then(() => console.log("async1 end"));
}
看例子:
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
async1();
setTimeout(() => {
console.log("timeout");
}, 0);
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");
步驟解析:
- 當(dāng)前任務(wù)隊列:宏任務(wù):[
<script>
]落追,微任務(wù): []
- 宏任務(wù):
-
輸出:
async1 start
- 遇到 async2,輸出:
async2
涯肩,并將 then(async1 end)加入微任務(wù) - 遇到 setTimeout轿钠,加入宏任務(wù)。
- 遇到 Promise病苗,輸出:
promise1
疗垛,直接 resolve,將 then(promise2)加入微任務(wù) -
輸出:
script end
-
輸出:
- 當(dāng)前任務(wù)隊列:微任務(wù)[promise2, async1 end]硫朦,宏任務(wù)[timeout]
- 微任務(wù):
-
輸出:
promise2
- promise2 出隊
-
輸出:
async1 end
- async1 end 出隊
- 微任務(wù)隊列清空
-
輸出:
- 當(dāng)前任務(wù)隊列:微任務(wù)[]贷腕,宏任務(wù)[timeout]
- 宏任務(wù):
-
輸出:
timeout
- timeout 出隊,宏任務(wù)清空
-
輸出:
注意:任務(wù)隊列(宏任務(wù)和微任務(wù))是棧(Stack)結(jié)構(gòu)咬展,執(zhí)行時遵循先進(jìn)后出(LIFO) 的原則
setTimerout 并不準(zhǔn)確
由上我們已經(jīng)知道了 setTimeout 是一個宏任務(wù)泽裳,會被添加到宏任務(wù)隊列當(dāng)中去,按順序執(zhí)行破婆,如果前面有涮总。
setTimeout() 的第二個參數(shù)是為了告訴 JavaScript 再過多長時間把當(dāng)前任務(wù)添加到隊列中。
如果隊列是空的祷舀,那么添加的代碼會立即執(zhí)行瀑梗;如果隊列不是空的,那么它就要等前面的代碼執(zhí)行完了以后再執(zhí)行蔑鹦。
看代碼:
const s = new Date().getSeconds();
console.log("script start");
new Promise((resolve) => {
console.log("promise");
resolve();
}).then(() => {
console.log("then1");
while (true) {
if (new Date().getSeconds() - s >= 4) {
console.log("while");
break;
}
}
});
setTimeout(() => {
console.log("timeout");
}, 2000);
console.log("script end");
因為then是一個微任務(wù)夺克,會先于setTimeout執(zhí)行,所以嚎朽,雖然setTimeout是在兩秒后加入的宏任務(wù)铺纽,但是因為then中的在while操作被延遲了4s,所以一直推遲到了4s秒后才執(zhí)行的setTimeout哟忍。
所以輸出的順序是:script start狡门、promise、script end锅很、then1其馏。
四秒后輸出:while、timeout
注意:關(guān)于 setTimeout 要補(bǔ)充的是爆安,即便主線程為空叛复,0 毫秒實際上也是達(dá)不到的。根據(jù) HTML 的標(biāo)準(zhǔn),最低是 4 毫秒褐奥。有興趣的同學(xué)可以自行了解咖耘。
總結(jié)
有個小 tip:從規(guī)范來看,microtask 優(yōu)先于 task 執(zhí)行撬码,所以如果有需要優(yōu)先執(zhí)行的邏輯儿倒,放入 microtask 隊列會比 task 更早的被執(zhí)行。
最后的最后呜笑,記住夫否,JavaScript 是一門單線程語言,異步操作都是放到事件循環(huán)隊列里面叫胁,等待主執(zhí)行棧來執(zhí)行的凰慈,并沒有專門的異步執(zhí)行線程。