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

衆所周知,JS 是一門單線程語言,但是瀏覽器又能很好的處理異步請求,那麼究竟是爲何呢?前端

JS 的執行環境通常是瀏覽器和 Node.js,二者稍有不一樣,這裏只討論瀏覽器環境下的狀況。面試

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

  • 同步任務:好比聲明語句、for、賦值等,讀取後依據從上到下從左到右,當即執行。
  • 異步任務:好比 ajax 網絡請求,setTimeout 定時函數等都屬於異步任務。異步任務會經過任務隊列(Event Queue)的機制(先進先出的機制)來進行協調。

任務隊列(Event Queue)

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

  • 宏任務主要包括:scrip(JS 總體代碼)、setTimeout、setInterval、setImmediate、I/O、UI 交互
  • 微任務主要包括:Promise(重點關注)、process.nextTick(Node.js)、MutaionObserver

任務隊列的執行過程是:先執行一個宏任務,執行過程當中若是產出新的宏/微任務,就將他們推入相應的任務隊列,以後在執行一隊微任務,以後再執行宏任務,如此循環。以上不斷重複的過程就叫作 Event Loop(事件循環)promise

每一次的循環操做被稱爲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>中的代碼):網絡

    1. 輸出: script start
    2. 遇到 setTimeout,加入宏任務隊列,當前宏任務隊列(setTimeout)
    3. 遇到 promise,加入微任務,當前微任務隊列(promise1)
    4. 輸出:script end
  2. 微任務:執行微任務隊列(promise1)數據結構

    1. 輸出:promise1,then 以後產生一個微任務,加入微任務隊列,當前微任務隊列(promise2)
    2. 執行 then,輸出promise2
  3. 執行渲染操做,更新界面(敲黑板劃重點)。
  4. 宏任務:執行 setTimeout異步

    1. 輸出: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>]
  1. 宏任務:

    1. 輸出: script start
    2. 遇到 timeout1,加入宏任務
    3. 遇到 Promise,輸出promise1,直接 resolve,將 then 加入微任務,遇到 timeout2,加入宏任務。
    4. 輸出script end
    5. 宏任務第一個執行結束
  • 當前任務隊列:微任務[then1],宏任務[timeou1, timeout2]
  1. 微任務:

    1. 執行 then1,輸出then1
    2. 微任務隊列清空
  • 當前任務隊列:微任務[],宏任務[timeou1, timeout2]
  1. 宏任務:

    1. 輸出timeout1
    2. 輸出timeout2
  • 當前任務隊列:微任務[],宏任務[timeou2]
  1. 微任務:

    1. 爲空跳過
  • 當前任務隊列:微任務[],宏任務[timeou2]
  1. 宏任務:

    1. 輸出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>],微任務: []
  1. 宏任務:

    1. 輸出:async1 start
    2. 遇到 async2,輸出:async2,並將 then(async1 end)加入微任務
    3. 遇到 setTimeout,加入宏任務。
    4. 遇到 Promise,輸出:promise1,直接 resolve,將 then(promise2)加入微任務
    5. 輸出:script end
  • 當前任務隊列:微任務[promise2, async1 end],宏任務[timeout]
  1. 微任務:

    1. 輸出:promise2
    2. promise2 出隊
    3. 輸出:async1 end
    4. async1 end 出隊
    5. 微任務隊列清空
  • 當前任務隊列:微任務[],宏任務[timeout]
  1. 宏任務:

    1. 輸出:timeout
    2. 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 毫秒。有興趣的同窗能夠自行了解。

<!-- ### 異步渲染策略 -->
<!-- 以 Vue 爲例 nextTick -->

總結

有個小 tip:從規範來看,microtask 優先於 task 執行,因此若是有須要優先執行的邏輯,放入 microtask 隊列會比 task 更早的被執行。

最後的最後,記住,JavaScript 是一門單線程語言,異步操做都是放到事件循環隊列裏面,等待主執行棧來執行的,並無專門的異步執行線程。

參考

相關文章
相關標籤/搜索