一次搞懂 - JS 事件循環之宏任務和微任務

作者:九旬

來源:SegmentFault 思否社區

衆所周知,JS 是一門單線程語言,可是瀏覽器又能很好的處理異步請求,那麼到底是爲什麼呢?

JS 的執行環境一般是瀏覽器和 Node.js,兩者稍有不同,這裏只討論瀏覽器環境下的情況。

JS 執行過程中會產生兩種任務,分別是:同步任務和異步任務。

任務隊列(Event Queue)

任務隊列中的任務也分爲兩種,分別是:宏任務(Macro-take)和微任務(Micro-take)

任務隊列的執行過程是:先執行一個宏任務,執行過程中如果產出新的宏 / 微任務,就將他們推入相應的任務隊列,之後在執行一隊微任務,之後再執行宏任務,如此循環。以上不斷重複的過程就叫做 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");

按照上面的內容,分析執行步驟:

  1. 宏任務:執行整體代碼(相當於<script>中的代碼):

  2. 輸出: script start

  3. 遇到 setTimeout,加入宏任務隊列,當前宏任務隊列 (setTimeout)

  4. 遇到 promise,加入微任務,當前微任務隊列 (promise1)

  5. 輸出:script end

  6. 微任務:執行微任務隊列(promise1)

  7. 輸出:promise1,then 之後產生一個微任務,加入微任務隊列,當前微任務隊列(promise2)

  8. 執行 then,輸出promise2

  9. 執行渲染操作,更新界面(敲黑板劃重點)。

  10. 宏任務:執行 setTimeout

  11. 輸出: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");

步驟解析:

  1. 宏任務:

  2. 輸出: script start

  3. 遇到 timeout1,加入宏任務

  4. 遇到 Promise,輸出promise1,直接 resolve,將 then 加入微任務,遇到 timeout2,加入宏任務。

  5. 輸出script end

  6. 宏任務第一個執行結束

  1. 微任務:

  2. 執行 then1,輸出then1

  3. 微任務隊列清空

  1. 宏任務:

  2. 輸出timeout1

  3. 輸出timeout2

  1. 微任務:

  2. 爲空跳過

  1. 宏任務:

  2. 輸出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");

步驟解析:

  1. 宏任務:

  2. 輸出:async1 start

  3. 遇到 async2,輸出:async2,並將 then(async1 end)加入微任務

  4. 遇到 setTimeout,加入宏任務。

  5. 遇到 Promise,輸出:promise1,直接 resolve,將 then(promise2) 加入微任務

  6. 輸出:script end

  1. 微任務:

  2. 輸出:promise2

  3. promise2 出隊

  4. 輸出:async1 end

  5. async1 end 出隊

  6. 微任務隊列清空

  1. 宏任務:

  2. 輸出:timeout

  3. 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