前言
在JavaScript
中,任務被分為同步任務和異步任務。
- 同步任務:這些任務在主線程上順序執行,不會進入任務隊列,而是直接在主線程上排隊等待執行。每個同步任務都會阻塞后續任務的執行,直到它自身完成。常見的同步任務包括頁面的初始化、DOM操作和某些計算任務。
- 異步任務:與同步任務不同,異步任務不直接進入主線程執行,而是被放入任務隊列(
task queue
)中。只有當主線程空閑時,才會從任務隊列中取出任務來執行。異步任務不會阻塞主線程的執行。根據任務類型,異步任務又被分為宏任務和微任務。
一、事件循環是什么?
JavaScript
的事件循環(Event Loop
)是其運行時環境(如瀏覽器或Node.js
)處理異步操作和回調的一種機制。它允許JavaScript
在不阻塞單線程執行的情況下,響應用戶交互、處理網絡請求、定時器回調等異步事件。
二、事件循環的基本基本概念和工作原理
1. 調用棧(Call Stack
):
JavaScript
引擎有一個調用棧,用于跟蹤函數調用的順序。當一個函數調用發生時,它會被推入調用棧中。當函數執行完畢后,它會被從調用棧中彈出。
2. 任務隊列(Task Queue
):
當異步操作(如 setTimeout、setInterval、DOM 事件、Promise.resolve().then()
等)完成時,它們會生成一個任務(task
)(簡單理解為任務就是回調函數),并將這個任務放入相應的任務隊列中等待執行。
- 這些任務隊列包括宏任務隊列(
macrotask queue
)和微任務隊列(microtask queue
) - 宏任務隊列主要存放
script
(全局任務)、setTimeout
、setInterval
、setImmediate
(Node.js
環境)等;微任務隊列主要存放Promise
的回調函數、MutationObserver
(瀏覽器環境)等。
3. 事件循環:
事件循環的基本順序是:
- 當調用棧為空時(即沒有正在執行的函數),事件循環會查看任務隊列。
- 事件循環會率先查看微任務隊列。如果微任務隊列中有任務,它會將任務逐個取出并執行,直到微任務隊列為空。
- 然后,事件循環會查看宏任務隊列。如果宏任務隊列中有任務,它會將任務取出并執行。在執行宏任務的過程中,可能會產生新的微任務,這些微任務會被添加到微任務隊列的末尾。
- 當一個宏任務執行完畢后,事件循環會再次查看微任務隊列并執行其中的任務,這個過程會一直重復,直到所有的任務都被執行完畢。
- 這個過程會持續進行,形成了一個循環,這就是所謂的“事件循環”。
三、宏任務和微任務?
- 在
JavaScript
的事件循環中,任務的執行被分為兩種主要的類別:宏任務(MacroTask
)和微任務(MicroTask
)。這兩種任務類型在事件循環中的處理順序和方式有所不同。
1. 宏任務(MacroTask
)
宏任務通常包括:
script
(整體代碼)setTimeout
setInterval
setImmediate
(Node.js
環境)I/O
UI
渲染(瀏覽器會在每次事件循環結束后進行UI渲染)MessageChannel
(消息通道)postMessage
(一些HTML5 API
使用)requestAnimationFrame
(瀏覽器用于定時執行動畫)
宏任務創建后會被放入宏任務隊列中,JavaScript引擎會在當前執行棧清空后,從宏任務隊列中取出隊首任務執行。
2. 微任務(MicroTask
)
微任務通常包括:
Promise.then()
或Promise.catch()
MutationObserver
(HTML5
的API
,用于監聽DOM
變更)process.nextTick
(Node.js
環境)
與宏任務不同,微任務是在當前宏任務執行完成后立即執行的。在JavaScript
引擎執行完一個宏任務后,它會先查看微任務隊列,并執行所有的微任務,直到微任務隊列為空。然后,它會繼續取出并執行下一個宏任務。這個過程會不斷重復,形成JavaScript
的事件循環。
執行順序
考慮以下的示例:
javascript
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'); // 同步任務
盡管setTimeout的延遲被設置為0,但它的回調仍然會在所有的微任務之后執行。因此,上述代碼的輸出順序為:script start
script end
promise1
promise2
setTimeout
這是因為當JavaScript引擎執行到setTimeout時,它會將回調函數放入宏任務隊列,并繼續執行后續的代碼。當執行到Promise.then()時,它會將回調函數放入微任務隊列。在所有宏任務代碼執行完畢后,JavaScript引擎會先執行所有的微任務,然后再從宏任務隊列中取出并執行下一個宏任務。
四、練習
- 練習一:
console.log('Start'); // 同步任務// 宏任務1setTimeout(() => {console.log('Timeout callback'); // 同步任務Promise.resolve().then(() => {console.log('Promise 1'); // 微任務1Promise.resolve().then(() => {console.log('Promise 2'); // 微任務2Promise.resolve().then(() => {console.log('Promise 3'); // 微任務3執行完執行下一個宏任務});});});}, 0);// 宏任務2setTimeout(() => {console.log('Timeout222 callback'); // 6}, 0);
- 練習二:
// 開啟一個微任務,當dom修改時觸發const observer = new MutationObserver(function (mutationsList, observer) {console.log(mutationsList, observer)});const config = { attributes: true, childList: true, subtree: true };console.log('script start'); // 同步任務 1 (function () {console.log('自執行函數 '); // 同步任務 2})()// 宏任務2setTimeout(function () {Promise.resolve().then(function () {var element = document.getElementById('app');observer.observe(element, config);var child = document.getElementById('child');element.innerHTML = '<p>這是一段新的HTML內容。</p>';console.log('promise11'); // 同步任務 Promise.resolve().then(() => {console.log('promise11 callback 1'); // (3) 微任務 });Promise.resolve().then(() => {console.log('promise11 callback 2'); // (3) 微任務 });})console.log('setTimeout'); // 同步任務 }, 0);// 宏任務3setTimeout(() => {console.log(111);})Promise.resolve().then(function () {console.log('promise1'); // 微任務1 Promise.resolve().then(() => {console.log('promise1 callback 1'); // 微任務1-2 });Promise.resolve().then(() => {console.log('promise1 callback 2'); // 微任務 1-3 });}).then(function () {console.log('promise2'); // 微任務2 // 宏任務4setTimeout(() => {console.log('微任務內的宏任務'); // 宏任務隊列4Promise.resolve().then(() => {console.log('微任務2 promise callback'); // 微任務隊列4 });})Promise.resolve().then(() => {console.log('promise2 callback 1'); // 微任務2-1 });Promise.resolve().then(() => {console.log('promise2 callback 2'); // 微任務2-2 });})console.log('script end'); // 同步任務3// 執行同步任務,當遇到異步宏任務放入宏任務隊列,異步微任務放入微任務隊列// 所以執行順序// script start// 自執行函數// script end// promise1// promise1 callback 1// promise1 callback 2// promise2// promise2 callback 1// promise2 callback 2// ---宏任務2// setTimeout// promise11// MutationObserver// promise11 callback 1// promise11 callback 2// ---宏任務3// 111// ---宏任務4// 微任務內的宏任務// 微任務2 promise callback
- 練習三:
script
整體為何是宏任務
// 宏任務一
<script>console.log('script1') // 同步// 宏任務三setTimeout(() => {console.log('setTimeout1');// 宏任務五setTimeout(() => {console.log('setTimeout3');})})// 微任務Promise.resolve().then(() => {console.log('promise1');})
</script>// 宏任務二
<script>console.log('script2') // 同步// 宏任務四setTimeout(() => {console.log('setTimeout2');// 宏任務六setTimeout(() => {console.log('setTimeout4');})})// 微任務Promise.resolve().then(() => {console.log('promise2');})
</script>可以看出來script相當于setTimeOut開啟宏任務列表,執行完當前宏任務去執行微任務,微任務執行完畢,執行宏任務二,以此類推所以輸出結果:
script1
promise1
script2
promise2
setTimeout1
setTimeout2
setTimeout3
setTimeout4