天天看點

JS 中的事件循環(Event Loop)和微任務與宏任務完全解析

作者:網際網路進階架構師

介紹

首先,地球人都知道JS是單線程的,是以JS同時隻能執行一個任務,也就是隻有一個調用棧,先執行同步任務,再執行異步任務。

雖然HTML5允許JS腳本建立多個線程,但是子線程完全受主線程控制,且不得操作 DOM。是以,這個新标準并沒有改變JS單線程的本質。

什麼是異步任務

異步任務就是那些被引擎放在一邊,不進入主線程、而進入任務隊列的任務。

什麼是事件循環

事件循環(Event Loop)是 JavaScript 引擎處理異步任務的機制。它用來管理所有的任務隊列,包括宏任務和微任務隊列。當 JavaScript 引擎遇到異步任務時,會将其放入相應的任務隊列中,并繼續執行同步代碼,直到同步代碼執行完畢或遇到下一個異步任務。當目前的宏任務執行完畢後,JavaScript 引擎會按照一定的規則從微任務隊列中取出任務執行,直到微任務隊列為空;然後再從宏任務隊列中取出下一個宏任務執行。這個過程就是事件循環。

宏任務和微任務都有

宏任務有

事件的回調函數,新程式或子程式被直接執行\<script>,setTimeout()和setInterval() requestAnimationFrame, i/o操作,setImmediate, UI rendering

微任務有

promise.then() catch() finally() MutationObserver. Object.observe async/await node.js 中的process.nextTick() queueMicrotask()

事件循環怎麼算一輪呢?

事件循環(Event Loop)是一個持續運作的機制,它不斷地執行任務隊列中的任務。一輪事件循環通常包括以下幾個階段:

  1. 執行目前宏任務:從宏任務隊列中取出一個宏任務執行。
  2. 執行目前宏任務中産生的微任務:當一個宏任務執行完畢後,會立即處理所有已經排隊的微任務,按照它們被添加到微任務隊列的順序依次執行。
  3. 更新渲染:如果需要進行頁面渲染,會執行相應的渲染操作。
  4. 檢查是否有 Web Worker 任務:如果有,則執行 Web Worker 任務。
  5. 進入下一輪事件循環:檢查是否有新的宏任務需要執行。如果有,跳轉到第 1 步,否則繼續等待新的任務被添加到隊列中。 一輪事件循環的結束并不一定意味着整個程式的結束,它隻是按照上述流程完成了一次任務的執行。事件循環會持續不斷地運作,等待新的任務被添加到隊列中,并按照上述流程執行。

是以輸出順序為 同步任務->異步任務(微任務->宏任務)

或者 宏任務->微任務->宏任務

代碼例子

<script>
console.log("Start");
  
setTimeout(function () {

console.log("這是定時器");

}, 0);
  
new Promise(() => {

console.log("這是Promise構造函數");

resolve();

}).then(() => {

console.log("這是Promise.Then");

});

console.log("End");
</script>

           

事件循環流程

  • 整體script作為第一個宏任務進入主線程,遇到console.log(Start),輸出Start
  • 遇到setTimeout,其回調函數被分發到宏任務中
  • 遇到newPromise,new Promise構造函數執行,輸出"這是Promise構造函數"
  • 遇到then被分發到微任務中
  • 遇到console.log("End"),輸出End
  • 調用棧被清空以後 事件循環就會優先尋找微任務隊列裡面的任務
  • 我們發現了then在微任務裡面,執行輸出“這是Promise.then”
  • 第一輪事件循環結束,開始第二輪事件循環
  • 宏任務有setTimeout對應的回調函數,立即執行輸出“這是定時器”

輸出結果

  • Start
  • 這是Promise構造函數
  • End
  • 這是Promise.Then
  • 這是定時器

這次來個複雜的例子 宏任務嵌套微任務 微任務嵌套宏任務 這次把script這個大宏任務忽略 以同步任務角度開始看

async function async1() {

console.log("async1 start");

await async2();

console.log("async1 end");

}


async function async2() {

console.log("async2");

}

console.log("script start");

 

setTimeout(function () {

console.log("setTimeout1");

});

setTimeout(function () {

console.log("setTimeout2");

Promise.resolve().then(() => {

console.log("then1");

});

Promise.resolve().then(() => {

console.log("then2");

});

});

 
async1();

new Promise((res) => {

console.log("this is Promise");

res();

}).then(() => {

console.log("then3");
setTimeout(() => {

console.log("then3 setTimeout3");

});

});

console.log("script end");
           
  • 遇到函數async1 async2沒有執行跳過
  • 遇到console.log('script start'),輸出script start
  • 遇到setTimeout1其回調函數分發到宏任務中
  • 遇到setTimeout2其回調函數分發到宏任務中
  • 遇到async1()函數執行, 遇到console.log("async1 start"),輸出async start
  • 在async1函數中遇到await async2() async2()優先級高于await運算符 async2()函數執行
  • 在async2函數中遇到console.log("async2")輸出 async2
  • 回到async1函數中 ,由于async函數使用await後的語句會被放入一個回調函數中,是以await後續代碼分發到微任務中
  • 遇到new Promise構造函數中 console.log("this is Promise"),直接執行 輸出this is Promise
  • then方法被分發到微任務中
  • 遇到console.log("script end")
  • 同步任務執行完了 開始執行異步任務 根據eventloop先執行任務隊列中的微任務
  • 任務隊列先入先出 是以先輸出'async1 end' 後輸出 new Promise.then中的内容 then3,
  • new Promise.then()中遇到setTimeout放到宏任務隊列中,
  • 事件循環第一輪結束,開始第二輪
  • 宏任務隊列setTimeout1拿出來輸出
  • 事件循環第二輪結束,開始第三輪
  • setTimeout2 執行 輸出setTimeout1和setTimeout2
  • setTimeout2中有兩個.then方法分發到微任務 再執行輸出 then1和then2
  • 事件循環第三輪結束,開始第四輪
  • 最後一個宏任務 輸出 then3 setTimeout3

是以代碼輸出結果

  • script start
  • async1 start
  • async2
  • this is Promise
  • script end
  • async1 end
  • then3
  • setTimeout1
  • setTimeout2
  • then1
  • then2
  • then3 setTimeout3

需要注意的是,微任務的執行順序是按照它們被添加到微任務隊列的順序來執行的。即使某個微任務的産生時間晚于其他微任務,但如果它被添加到隊列較早,那麼它仍然會先于其他微任務執行。

文章到這裡就結束了,希望對你有所幫助

作者:ZhaiMou

連結:https://juejin.cn/post/7330300019022970915