javascript是一門單線程、非阻塞的語言。任何時候,都只有一個主線程來處理所有的任務。
當遇到異步事件時,並不會一直等待異步代碼返回結果,而是會將這個事件掛起,繼續執行執行棧中的其他任務。當異步事件返回結果後,js會將異步事件添加到另一個與主執行棧不同的隊列,稱之爲異步任務隊列。
被添加到異步任務隊列中的回調並不會立即執行,而是會等主執行棧的任務執行完畢後,再去檢查任務隊列(這裏的任務隊列,又分爲微任務和宏任務)裏是否有任務。首先會去讀取微任務隊列(micro task)是否有事件存在,如果不存在,接着會讀取宏任務隊列(macro task),把宏任務隊列中的事件回調,推入到主執行棧進行執行。如果存在,會依次讀取微任務隊列,直到所有的微任務都執行完畢,再去讀取宏任務隊列的事件,把其對應的回調代碼推入到主執行棧中進行執行,如此反覆執行,形成循環----也就是我們所說的事件循環。
爲了更明確事件循環的流程,可通過 下圖 或 僞代碼 幫助記憶:
什麼是事件循環(記憶僞代碼):
// eventLoop是一個用作隊列的數組 //(先進,先出)
var eventLoop = [ ];
var event;
//“永遠”執行
while (true) {
// 一次tick
if (eventLoop.length > 0) {
// 拿到隊列中的下一個事件
event = eventLoop.shift();
// 現在,執行下一個事件
try {
event();
} catch (err) {
reportError(err);
}
}
}
宏任務(macro task) 主要包含:script、setTimeout、setInterval、I/O、UI 交互事件
微任務(micro task) 主要包含:Promise、MutaionObserver
接下來再來看我們經常遇到的面試考題:
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');
以上代碼執行流程爲:
- 會先執行同步代碼:執行console.log(‘script start’);
- 接着遇到異步事件setTimeout,會添加到宏任務隊列等待執行。
- new Promise會立即執行,然後執行內部的console.log(‘promise1’);
- 接着又遇到new Promise內部的異步事件setTimeout,仍然添加到宏任務隊列等待執行;
- new Promise內的代碼執行完之後,會遇到promise.then異步事件,它爲微任務事件,所以添加到微任務等待執行;
- 接着又會執行下面的同步代碼console.log(‘script end’);
- 同步代碼執行完畢,會先讀微任務事件,這時微任務中含有promise.then(), 推入主執行棧執行,會打印 ‘then1’;
- 微任務執行完畢,去讀取宏任務中的事件,依次執行後打印 ‘timeout1’ 和 ‘timeout2’;
所以以上代碼打印結果爲:script start、promise1、script end、then1、timeout1、timeout2
注: 一定要清楚,setTimeout(…) 並沒有把你的回調函數掛在事件循環隊列中。它所做的是設定一個定時器。當定時器到時後,環境會把你的回調函數放在事件循環中,這樣,在未來某個時刻的 tick 會摘下並執行這個回調。
如果這時候事件循環中已經有 20 個項目了會怎樣呢? 你的回調就會等待。它得排在 其他項目後面——通常沒有搶佔式的方式支持直接將其排到隊首。這也解釋了爲什麼 setTimeout(…) 定時器的精度可能不高。大體說來,只能確保你的回調函數不會在指定的 時間間隔之前運行,但可能會在那個時刻運行,也可能在那之後運行,要根據事件隊列的 狀態而定。
?小結:微任務優先於宏任務執行,所以如果有需要優先執行的異步邏輯,可放入 micro task 隊列會比 macro task 更早的被執行。