詳解JavaScript的任務、微任務、佇列以及程式碼執行順序

前端小智發表於2019-07-25

原文:jakearchibald.com/2015/tasks-…

譯者:前端小智

你知道的越多,你不知道的越多

點贊再看,養成習慣


本文 GitHub:github.com/qq449245884… 上已經收錄,更多往期高贊文章的分類,也整理了很多我的文件,和教程資料。歡迎Star和完善,大家面試可以參照考點複習,希望我們一起有點東西。

為了保證的可讀性,本文采用意譯而非直譯。

思考下面 JavaScript 程式碼:

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');
複製程式碼

控制檯列印的順序是怎樣的?

答案

正確的答案是:script start, script end, promise1, promise2, setTimeout,但是由於瀏覽器實現支援不同導致結果也不一致。

Microsoft Edge、Firefox 40、iOS Safari和桌面Safari 8.0.8 列印promise1promise2之前會先列印 setTimeout —— 這似乎是瀏覽器廠商相互競爭導致的實現不同。這真的很奇怪,因為 Firefox 39 和 Safari 8.0.7 結果總是正確的。

為什麼會這樣

要理解這一點,需要了解**事件迴圈**如何處理任務和微任務。

每個“執行緒”都有自己的事件迴圈,因此每個 web worker 都有自己的事件迴圈,因此可以獨立執行,而來自同域的所有視窗共享一個事件迴圈,所以它們可以同步地通訊。

事件迴圈持續執行,直到清空 Tasks 佇列的任務。一個事件迴圈有多個任務源,這些任務源保證了該源中的執行順序(比如IndexedDB定義了它們自己的規範),但是瀏覽器可以在每次迴圈中選擇哪個源來執行任務。這允許瀏覽器優先選擇效能敏感的任務,比如使用者輸入等。

Tasks 被放到任務源中,這樣瀏覽器就可以從內部進入JavaScript/DOM領域,並確保這些操作按順序進行。在Tasks 執行期間,瀏覽器可能更新渲染。從滑鼠點選到事件回撥需要排程一個任務,解析超文字標記語言也是如此。

setTimeout遲給定的時間,然後為它的回撥排程一個新任務。這就是為什麼setTimeout在列印script end之後列印,因為列印script end是第一個任務的一部分,而setTimeout在一個單獨的任務中。

**微任務**通常是針對當前執行指令碼之後應該立即發生的事情進行排程的,比如對一批操作進行響應,或者在不影響整個新任務的情況下進行非同步處理。

只要沒有其他JavaScript處於執行中期,並且在每個任務的末尾,微任務佇列就在回撥之後處理。在微任務期間排隊的任何其他微任務都會被新增到佇列的末尾並進行處理。微任務 包括 MutationObserver callbacks。例如上面的例子中的 promisecallback

一個settled狀態的promise 或者已經變成settled狀態(非同步請求被settled)的promise,會立刻將它的callback(then)放到微任務佇列裡面。

這確保了 promise 回撥是非同步的,即便promise已經變為settled狀態。因此一個已settledpromise呼叫.then(yey,nay)時將立即把一個微任務加入微任務佇列中。

這就是為什麼promise1promise2會在script end後列印,因為當前執行的指令碼必須在處理微任務之前完成。promise1promise2setTimeout之前列印,因為微任務總是在下一個任務之前發生。

好,一步一步的執行:

詳解JavaScript的任務、微任務、佇列以及程式碼執行順序

瀏覽器之間會有什麼不同?

一些瀏覽器的列印的順序是 script start, script end, setTimeout, promise1, promise2。它們在setTimeout之後執行promise回撥。很可能他們呼叫promise回撥是作為新任務的一部分,而不是作為一個微任務。

這也是可以理解的,因為promise來自 ECMAScript 而不是 HTML。ECMAScript 有“作業”的概念,類似於微任務,但是除了模糊的郵件列表討論之外,這種關係並不明確。然而,普遍的共識是,promise應該是微任務佇列的一部分並且有充足的理由。

promise 看作任務會導致效能問題,因為回撥沒有必要因為任務相關的事(比如渲染)而延遲執行。它還會由於與其他任務源的互動而導致非確定性,並可能中斷與其他api的互動,稍後將詳細介紹。

這裡有一條 Edge 反饋,它錯誤地將 promises 當作 任務。WebKit nightly 做對了,所以我認為 Safari 最終會修復,而 Firefox 43 似乎已經修復。

如何判斷某些東西是否使用任務或微任務

動手試一試是一種辦法,檢視相對於promisesetTimeout如何列印,儘管這取決於實現是否正確。

一種方法是檢視規範: 將一個任務加入佇列: step 14 of setTimeout

將 microtask 加入佇列:step 5 of queuing a mutation record

如上所述,ECMAScript 將微任務稱為作業: 呼叫 EnqueueJob 將一個 微任務加入佇列:step 8.a of PerformPromiseThen

等級一 boss打怪

下面是一段html程式碼:

<div class="outer">
  <div class="inner"></div>
</div>
複製程式碼

給出下面的JS程式碼,如果點選div.inner將會列印出什麼呢?

// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

// Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
  console.log('mutate');
}).observe(outer, {
  attributes: true
});

// Here's a click listener…
function onClick() {
  console.log('click');

  setTimeout(function() {
    console.log('timeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('promise');
  });

  outer.setAttribute('data-random', Math.random());
}

// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
複製程式碼

在偷看答案前先試一試

試一試

詳解JavaScript的任務、微任務、佇列以及程式碼執行順序

和你猜想的有不同嗎?如果是,你得到的結果可能也是正確的。不幸的是,瀏覽器實現並不統一,下面是各個瀏覽器下測試結果:

詳解JavaScript的任務、微任務、佇列以及程式碼執行順序

誰是正確的?

排程'click'事件是一項任務。 Mutation observer 和 promise 回撥被列為微任務。 setTimeout 回撥列為任務。 因此執行過程如下:

詳解JavaScript的任務、微任務、佇列以及程式碼執行順序

所以 Chrome 是對的。對我來說新發現是,微任務在回撥之後執行(只要沒有其它的 Javascript 在執行),我原以為它只能在一個任務的末尾執行。

瀏覽器出了什麼問題?

對於 mutation callbacks,Firefox 和 Safari 都正確地在內部區域和外部區域單擊事件之間執行完畢,清空了微任務佇列,但是 promises 列隊的處理看起來和chrome不一樣。這多少情有可原,因為作業和微任務的關係不清楚,但是我仍然期望在事件回撥之間處理 Firefox ticket. Safari ticket.

對於 Edge,我們已經看到它錯誤的將 promises 當作任務,它也沒有在單擊回撥之間清空微任務佇列,而是在所有單擊回撥執行完之後清空,於是總共只有一個 mutate 在兩個 click 之後列印。

等級一 boss打怪升級

仍然使用上面的例子,假如我們執行下面程式碼會怎麼樣:

inner.click();
複製程式碼

跟之前一樣,它會觸發 click 事件,但這次是通過 JS 呼叫的。

試一試

詳解JavaScript的任務、微任務、佇列以及程式碼執行順序

下面是各個瀏覽器的執行情況:

詳解JavaScript的任務、微任務、佇列以及程式碼執行順序

我發誓我一直在從Chrome中得到不同的結果,我已經更新了這張圖表很多次了,我以為我在錯誤地測試Canary。如果你在Chrome中得到不同的結果,請在評論中告訴我是哪個版本。

為什麼不同?

應該是這樣的:

詳解JavaScript的任務、微任務、佇列以及程式碼執行順序

所以正確的順序是:click, click, promise, mutate, promise, timeout, timeout,似乎 Chrome 是對的。

以前,這意味著微任務在偵聽器回撥之間執行,但.click()會導致事件同步排程,因此呼叫.click()的指令碼仍然在回撥之間的堆疊中。 上述規則確保微任務不會中斷執行中期的JavaScript。 這意味著我們不處理偵聽器回撥之間的微任務佇列,它們在兩個偵聽器之後處理。

總結

任務按順序執行,瀏覽器可以在它們之間進行渲染:

微任務按順序執行,並執行:

  • 在每個回撥之後,只要沒有其它程式碼正在執行。

  • 在每個任務的末尾。

程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug

交流(歡迎加入群,群工作日都會發紅包,互動討論技術)

乾貨系列文章彙總如下,覺得不錯點個Star,歡迎 加群 互相學習。

github.com/qq449245884…

我是小智,公眾號「大遷世界」作者,對前端技術保持學習愛好者。我會經常分享自己所學所看的乾貨,在進階的路上,共勉!

關注公眾號,後臺回覆福利,即可看到福利,你懂的。

詳解JavaScript的任務、微任務、佇列以及程式碼執行順序

相關文章