原文地址(英): https://jakearchibald.com/201...
當我告訴Matt Gaunt(做者的同事),我正在謀劃寫一篇關於在瀏覽器事件循環(event loop)體系中微任務( microtask )的隊列和執行的文章時,他說:「實話告訴你Jake,我對這篇文章是不會感興趣的」。好吧,無論怎樣,既然我已經寫了那就讓咱們坐下來好好享受它,好嗎?html
事實上,若是視頻更符合你的胃口,那麼Philips Roberts 在JSConf上關於event loop的演講會是很好的參考(該演講不涉及微任務(microtask),可是對事件循環的其餘部分都講得很是好),閒話少說,開始咱們的內容。html5
如下是一小段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');
想一想看控制檯會按照什麼樣的順序打印結果呢?git
正確的答案是:script start
, script end
, promise1
, promise2
, setTimeout
,可是在不一樣的瀏覽器上結果可能會有所不一樣。github
Microsoft Edge, Firefox 40, iOS Safari和桌面版Safari 8.0.8會在promise1
,promise2
以前打印出setTimeout
,雖然這多是瀏覽器廠商間各自競爭的結果,但這未免有些奇怪,由於Firefox 39和Safari 8.0.7獲得的結果始終是正確的。web
爲了搞清楚原因,你須要明白事件循環(event loop)是如何處理任務(tasks)和微任務(microtasks)的.當這些名詞第一次出現的時候,你可能會感到頭疼,不要緊,深呼吸...api
每個"線程"都擁有屬於本身的事件循環(event loop),也就意味着每個web worker都會存在自有的事件循環並獨立運行互不干擾。然而全部同源窗口之間共享一個事件循環(event loop),這樣它們就能夠同步通訊了(譯者注:根據HTML5.2規範,事件循環分兩種,一種是瀏覽器上下文的,一種是web worker的)。事件循環(event loop)老是不斷的運行,執行隊列中的任務(task)。一個事件循環存在多個任務源,這確保了任務在特定任務源的執行順序(譯者注:同一個任務源的任務將被添加到相同任務隊列,不一樣任務源的任務可能被添加到不一樣任務隊列),可是在每一次的循環中,瀏覽器會自主選擇哪一個源的任務優先執行,這確保了一些性能敏感的任務的優先級,好比用戶輸入。promise
任務(tasks,譯者注:也叫macro-task)被放到任務源中,瀏覽器內部執行轉移到JavaScript/DOM領域,而且確保這些 tasks按序執行。在tasks執行期間,瀏覽器可能更新渲染。來自鼠標點擊的事件回調須要安排一個task,解析HTML和setTimeout一樣須要。瀏覽器
setTimeout等待了給定的延遲時間以後就會爲它的回調建立一個新的任務。這就是爲何setTimeout在script end
以後打印script start
,由於script end
是歸屬於第一個任務,而setTimeout
對應的是另外一個任務,至此,咱們快要搞清楚了,我須要大家有足夠的耐心看完下一個部分app
微任務(Microtasks)隊列一般用於存放一些任務,這些任務應該在正在執行的腳本以後當即執行,好比對一批動做做出反應,或者操做異步執行避免建立整個新任務形成的性能浪費。每次事件循環中,若是沒有其餘JavaScript運行而且任務(task)都執行完畢了,那麼微任務就會在回調以後被執行。在微任務中排隊的任何其餘微任務將被添加到隊列的末尾並進行處理。微任務包括 MutationObserver
、Promise
的回調(譯者注:微任務包括:process.nextTick(Nodejs), Promises, Object.observe, MutationObserver;任務(tasks)包括:script(總體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering)。webapp
一個settled
狀態的promise
(直接調用resolve
或者reject
)或者已經變成settled
狀態(異步請求被settled
)的promise
,會馬上將它的callback
(then)放到microtask隊列裏面。這就能保證promise
的回調是異步的,即使promise
已經變爲settled
狀態。所以一個已settled
的promise
調用.then(yey,nay)
時將當即把一個microtask任務加入microtasks任務隊列。這就是爲何 promise1
和 promise2
在 script end
以後打印,由於正在運行的代碼必須在處理 microtasks 以前完成。promise1
和 promise2
在 setTimeout
以前打印,由於 microtasks 老是在下一個 task 以前執行。
讓咱們一步一步分析,(譯者注:跳轉到原文step by step示例,這對理解本文很是有用)
一些瀏覽器打印出來的結果是:script start
,script end
,setTimeout
,promise1
,promise2
。這些瀏覽器在promise回調以前調用了setTimeout。這極可能是瀏覽器把promise回調當作是新任務(task )的一部分而不是微任務(microtask)。
這種錯誤某種程度上是能夠被原諒的,由於promises規範來源於ECMAScript而不是HTML。ECMAScript定義了相似微任務的「jobs」概念,可是除了一些模糊的郵件討論以外,這種關係(jobs和microtasks)並不明確。但promises應該做爲微任務的一部分這是廣泛的共識。
把promise當作是任務將會致使一些性能問題,回調可能沒有必要由於某些相關任務(好比渲染)而被延遲。因爲與其餘任務源的交互這也會致使一些不肯定性,而且會中斷與其餘Api的交互。
把promise歸類爲微任務已是很急迫的事情了。Webkit(Safari內核)一直都在作正確的事情,我想Safari最終會解決這和問題,事實上,Firefox43已經修復了這個問題。
真正有趣的是,Safari和Firefox在這裏都經歷了一次迴歸,從那之後問題就被修復了。我想知道這是否是一個巧合。