從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實現事件循環的方式與規範沒有關係,但具體爲什麼會打印出不同的效果。。求大神@我

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章