一次搞懂 - JS 事件循環之宏任務和微任務
作者:九旬
來源:SegmentFault 思否社區
衆所周知,JS 是一門單線程語言,可是瀏覽器又能很好的處理異步請求,那麼到底是爲什麼呢?
JS 的執行環境一般是瀏覽器和 Node.js,兩者稍有不同,這裏只討論瀏覽器環境下的情況。
JS 執行過程中會產生兩種任務,分別是:同步任務和異步任務。
-
同步任務:比如聲明語句、for、賦值等,讀取後依據從上到下從左到右,立即執行。
-
異步任務:比如 ajax 網絡請求,setTimeout 定時函數等都屬於異步任務。異步任務會通過任務隊列 (Event Queue) 的機制(先進先出的機制)來進行協調。
任務隊列(Event Queue)
任務隊列中的任務也分爲兩種,分別是:宏任務(Macro-take)和微任務(Micro-take)
-
宏任務主要包括:scrip(JS 整體代碼)、setTimeout、setInterval、setImmediate、I/O、UI 交互
-
微任務主要包括:Promise(重點關注)、process.nextTick(Node.js)、MutaionObserver
任務隊列的執行過程是:先執行一個宏任務,執行過程中如果產出新的宏 / 微任務,就將他們推入相應的任務隊列,之後在執行一隊微任務,之後再執行宏任務,如此循環。以上不斷重複的過程就叫做 Event Loop(事件循環)。
每一次的循環操作被稱爲 tick。
理解微任務和宏任務的執行執行過程
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");
按照上面的內容,分析執行步驟:
-
宏任務:執行整體代碼(相當於
<script>
中的代碼): -
輸出:
script start
-
遇到 setTimeout,加入宏任務隊列,當前宏任務隊列 (setTimeout)
-
遇到 promise,加入微任務,當前微任務隊列 (promise1)
-
輸出:
script end
-
微任務:執行微任務隊列(promise1)
-
輸出:
promise1
,then 之後產生一個微任務,加入微任務隊列,當前微任務隊列(promise2) -
執行 then,輸出
promise2
-
執行渲染操作,更新界面(敲黑板劃重點)。
-
宏任務:執行 setTimeout
-
輸出:
setTimeout
Promise 的執行
new Promise(..)
中的代碼,也是同步代碼,會立即執行。只有then
之後的代碼,纔是異步執行的代碼,是一個微任務。
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");
步驟解析:
- 當前任務隊列:微任務: [], 宏任務:[
<script>
]
-
宏任務:
-
輸出:
script start
-
遇到 timeout1,加入宏任務
-
遇到 Promise,輸出
promise1
,直接 resolve,將 then 加入微任務,遇到 timeout2,加入宏任務。 -
輸出
script end
-
宏任務第一個執行結束
- 當前任務隊列:微任務 [then1],宏任務 [timeou1, timeout2]
-
微任務:
-
執行 then1,輸出
then1
-
微任務隊列清空
- 當前任務隊列:微任務 [],宏任務 [timeou1, timeout2]
-
宏任務:
-
輸出
timeout1
-
輸出
timeout2
- 當前任務隊列:微任務 [],宏任務 [timeou2]
-
微任務:
-
爲空跳過
- 當前任務隊列:微任務 [],宏任務 [timeou2]
-
宏任務:
-
輸出
timeout2
async/await 的執行
async 和 await 其實就是 Generator 和 Promise 的語法糖。
async 函數和普通 函數沒有什麼不同,他只是表示這個函數里有異步操作的方法,並返回一個 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");
步驟解析:
- 當前任務隊列:宏任務:[
<script>
],微任務: []
-
宏任務:
-
輸出:
async1 start
-
遇到 async2,輸出:
async2
,並將 then(async1 end)加入微任務 -
遇到 setTimeout,加入宏任務。
-
遇到 Promise,輸出:
promise1
,直接 resolve,將 then(promise2) 加入微任務 -
輸出:
script end
- 當前任務隊列:微任務 [promise2, async1 end],宏任務 [timeout]
-
微任務:
-
輸出:
promise2
-
promise2 出隊
-
輸出:
async1 end
-
async1 end 出隊
-
微任務隊列清空
- 當前任務隊列:微任務 [],宏任務 [timeout]
-
宏任務:
-
輸出:
timeout
-
timeout 出隊,宏任務清空
"任務隊列" 是一個事件的隊列(也可以理解成消息的隊列),IO 設備完成一項任務,就在 "任務隊列" 中添加一個事件,表示相關的異步任務可以進入 "執行棧" 了。主線程讀取 "任務隊列",就是讀取裏面有哪些事件。
"任務隊列" 中的事件,除了 IO 設備的事件以外,還包括一些用戶產生的事件(比如鼠標點擊、頁面滾動等等)。只要指定過回調函數,這些事件發生時就會進入 "任務隊列",等待主線程讀取。
所謂 "回調函數"(callback),就是那些會被主線程掛起來的代碼。異步任務必須指定回調函數,當主線程開始執行異步任務,就是執行對應的回調函數。
"任務隊列" 是一個先進先出的數據結構,排在前面的事件,優先被主線程讀取。主線程的讀取過程基本上是自動的,只要執行棧一清空,"任務隊列" 上第一位的事件就自動進入主線程。但是,由於存在後文提到的 "定時器" 功能,主線程首先要檢查一下執行時間,某些事件只有到了規定的時間,才能返回主線程。
----JavaScript 中沒有任何代碼時立即執行的,都是進程空閒時儘快執行
setTimerout 並不準確
由上我們已經知道了 setTimeout 是一個宏任務,會被添加到宏任務隊列當中去,按順序執行,如果前面有。
setTimeout() 的第二個參數是爲了告訴 JavaScript 再過多長時間把當前任務添加到隊列中。
如果隊列是空的,那麼添加的代碼會立即執行;如果隊列不是空的,那麼它就要等前面的代碼執行完了以後再執行。
看代碼:
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 是一個微任務,會先於 setTimeout 執行,所以,雖然 setTimeout 是在兩秒後加入的宏任務,但是因爲 then 中的在 while 操作被延遲了 4s,所以一直推遲到了 4s 秒後才執行的 setTimeout。
所以輸出的順序是:script start、promise、script end、then1。
四秒後輸出:while、timeout
注意:關於 setTimeout 要補充的是,即便主線程爲空,0 毫秒實際上也是達不到的。根據 HTML 的標準,最低是 4 毫秒。有興趣的同學可以自行了解。
總結
有個小 tip:從規範來看,microtask 優先於 task 執行,所以如果有需要優先執行的邏輯,放入 microtask 隊列會比 task 更早的被執行。
最後的最後,記住,JavaScript 是一門單線程語言,異步操作都是放到事件循環隊列裏面,等待主執行棧來執行的,並沒有專門的異步執行線程。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/INlLSh0aXKnXr9ZVzCDi5w