1. 程式人生 > >從HTML5與PromiseA+規範來看事件迴圈

從HTML5與PromiseA+規範來看事件迴圈

寫在最前

本次分享一下從HTML5PromiseA+規範來迅速理解一波事件迴圈中的microtask 與macrotask。

歡迎關注我的部落格,不定期更新中——

JavaScript小眾系列開始更新啦

——何時完結不確定,寫多少看我會多少!這是已經更新的地址:

這個系列旨在對一些人們不常用遇到的知識點,以及可能常用到但不曾深入瞭解的部分做一個重新梳理,雖然可能有些部分看起來沒有什麼用,因為平時開發真的用不到!但個人認為糟粕也好精華也罷裡面全部蘊藏著JS一些偏本質的東西或者說底層規範,如果能適當避開舒適區來看這些小細節,也許對自己也會有些幫助~文章更新在我的部落格,歡迎不定期關注。

先來看段程式碼

setTimeout(function() {
  console.log('setTimeout1');
    Promise.resolve().then(function() {
      console.log('promise1');
    }).then(function() {
      console.log('promise2');
    })
}, 0);
setTimeout(function() {
  console.log('setTimeout2');
    Promise.resolve().then(function() {
      console.log('promise3');
    }).then(function() {
      console.log('promise4');
    })
}, 0);

從這段程式碼中我們發現裡面有兩個定時器setTimeout,每個定時器中還嵌套了Promise。我相信熟悉microtask 與macrotask任務佇列的童鞋能很快的知曉答案,這個東西給我的感覺就是清者自清。

so 結果是什麼?

/* 請在新版chrome中列印結果
    setTimeout1
    promise1
    promise2
    setTimeout2
    promise3
    promise4
*/

why?

不做解釋直接看下規範中怎麼說的:

There must be at least one browsing context event loop per user agent, and at most one per unit of related similar-origin browsing contexts. An event loop has one or more task queues.

一個瀏覽器環境下只能有一個事件迴圈,同時迴圈中是可以存在多個任務佇列的。
同時我們接著看規範中對event-loop執行過程是如何規定的:

1.Let oldestTask be the oldest task on one of the event loop's task queues.

2.Set the event loop's currently running task to oldestTask.

3.Run oldestTask.

4.Set the event loop's currently running task back to null.

5.Remove oldestTask from its task queue.

6.Microtasks: Perform a microtask checkpoint.

7.Update the rendering

其中的task queues,就是之前提到的macrotask,中文可以翻譯為巨集任務。顧名思義也就是正常的一些回撥執行,比如IO,setTimeout等。簡單來說當事件迴圈開始後,會將task queues最先進棧的任務執行,之後移出,進行到第六步,做microtask的檢測。發現有microtask的任務那麼會依照如下方式執行:

While the event loop's microtask queue is not empty:

//當microtask佇列中還有任務時,按照下面執行

1.Let oldestMicrotask be the oldest microtask on the event loop's microtask queue.

2.Set the event loop's currently running task to oldestMicrotask.

3.Run oldestMicrotask.

4.Set the event loop's currently running task back to null.

5.Remove oldestMicrotask from the microtask queue.

從這段規範可以看出,當執行了一個macrotask後會有一個迴圈來檢查microtask佇列中是否還存在任務,如果有就執行。這說明執行了一個macrotask(巨集任務)之後,會執行所有註冊了的microtask(微任務)。

一起看起來很正常對吧?

那麼如果微任務“巢狀”了呢?就像一開始作者給出的那段程式碼一樣,promise呼叫了很多次.then方法。這種情況文件中有做出規定麼?有的。

If, while a compound microtask is running, the user agent is required to execute a compound microtask subtask to run a series of steps, the user agent must run the following steps:

1.Let parent be the event loop's currently running task (the currently running compound microtask).

2.Let subtask be a new task that consists of running the given series of steps. The task source of such a microtask is the microtask task source. This is a compound microtask subtask.

3.Set the event loop's currently running task to subtask.

4.Run subtask.

5.Set the event loop's currently running task back to parent.

簡單來說如果有“巢狀”的情況,註冊的任務都是microtask,那麼就會一股腦得全部執行。

小結

通過上面對文件的解讀我們可以知道以下幾件事:

  1. 一個執行環境有一個事件迴圈。PS:有關web worker的概念作者也不太清楚,有興趣的童鞋可以查查
  2. 重點# 一個事件迴圈有多個任務佇列。目前來看是實現了兩個佇列

  3. 佇列分為macrotask巨集任務佇列與microtask微任務佇列
  4. 回撥的任務會被分配到macrotask與microtask中,具體分配見下文。
  5. 執行一個巨集任務,將已經註冊的所有微任務,包括有“巢狀”的全部執行。
  6. 執行下一個巨集任務,重複步驟5

那麼還剩一件事情就是什麼任務是macrotask,什麼是microtask?

image.png
image.png
這張圖來源一篇翻譯PromisA+的文章,裡面所提到的關於任務的分類。

但是!我對於setImmediate與process.nextTick的行為持懷疑態度。理由最後說!不過在瀏覽器執行環境中我們不需要關係上面那兩種事件。

測試一下程式碼

在本文一開始就提出,這段程式碼要在新版chrome中執行才會得到正確結果。那麼不在chrome中呢?

safari
safari

舉個例子,別的作者不一一測試了,這是safari中的結果。我們可以看到順序被打亂了。so為什麼我執行了一樣的程式碼結果卻不同?
個人認為若出現結果不同的情況是由於不同執行環境(chrome, safari, node .etc)將回調需要執行的任務所劃分到的任務佇列PromiseA+規範中所提到的任務佇列中的任務劃分準則執行不一致導致的。也就是Promise可能被劃分到了macrotask中。有興趣深入瞭解的童鞋可以看下這篇tasks-microtasks-queues-and-schedules.

拋一個作者也解釋不清的問題

細心的童鞋可能發現我一直強調的js執行環境是瀏覽器下的事件迴圈情況。那麼node中呢?

setTimeout(function() {
  console.log('setTimeout1');
    Promise.resolve().then(function() {
      console.log('promise1');
    }).then(function() {
      console.log('promise2');
    })
}, 0);
setTimeout(function() {
  console.log('setTimeout2');
    Promise.resolve().then(function() {
      console.log('promise3');
    }).then(function() {
      console.log('promise4');
    })
}, 0);

還是這段程式碼,打印出來會不會有區別?多列印幾次結果一樣麼?為什麼會這樣?

我只能理解到node通過libuv實現事件迴圈的方式與規範沒有關係,但具體為什麼會打印出不同的效果。。求大神@我