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
,但它在瀏覽器支持方面至關野生那裏。javascript
Microsoft Edge,Firefox 40,iOS Safari和桌面Safari 8.0.8 setTimeout
以前promise1
和以後都進行了日誌記錄promise2
-儘管這彷佛是一種競爭情況。這真的很奇怪,由於Firefox 39和Safari 8.0.7始終如一地正確。html
要了解這一點,您須要瞭解事件循環如何處理任務和微任務。第一次遇到這個問題可能會讓您大吃一驚。深呼吸…java
每一個「線程」都有本身的事件循環,所以每一個Web工做者都有本身的事件循環,所以能夠獨立執行,而同一源上的全部窗口均可以共享事件循環,由於它們能夠同步通訊。事件循環持續運行,執行全部排隊的任務。事件循環具備多個任務源,這些任務源保證了該源中的執行順序(如IndexedDB之類的規範定義了它們的執行順序),可是瀏覽器能夠在循環的每一個循環中選擇從哪一個源中執行任務。這使瀏覽器能夠優先執行對性能敏感的任務,例如用戶輸入。好吧好吧,和我在一塊兒…git
計劃了任務,以便瀏覽器能夠從內部訪問JavaScript / DOM,並確保這些操做順序發生。在任務之間,瀏覽器能夠呈現更新。從鼠標單擊到事件回調,與分析HTML同樣須要安排任務,在上例中爲setTimeout
。es6
setTimeout
等待給定的延遲,而後爲其回調安排新任務。這就是爲何setTimeout
在以後script end
進行記錄的緣由,由於日誌記錄script end
是第一個任務的一部分,並setTimeout
記錄在單獨的任務中。是的,咱們幾乎已經完成了這一步,但我須要您在接下來的這段時間內保持堅強……github
Microtasks一般安排事情,應該當前執行腳本後直髮生,如反應批量的行動,或使一些異步而不採起一個全新的任務的處罰。只要沒有其餘JavaScript在執行中間,微任務隊列就會在回調以後進行處理,而且在每一個任務結束時進行處理。在微任務期間排隊的全部其餘微任務都將添加到隊列的末尾並進行處理。微任務包括變異觀察者回調,並如上例所示,承諾回調。promise
一旦承諾達成,或者若是已經達成,它將對微任務排隊以進行其反動回調。這樣能夠確保即便promise已經解決,promise回調也是異步的。所以,.then(yey, nay)
對已解決的諾言進行調用會當即使微任務排隊。這就是爲何promise1
並promise2
在以後記錄日誌的緣由script end
,由於當前正在運行的腳本必須在處理微任務以前完成。promise1
而且promise2
在以前記錄setTimeout
,由於微任務老是在下一個任務以前發生。瀏覽器
所以,請逐步:app
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
,setTimeout
,promise1
,promise2
。他們在以後運行promise回調setTimeout
。他們可能將promise回調稱爲新任務的一部分,而不是微任務。dom
這是能夠原諒的,由於承諾來自ECMAScript而不是HTML。ECMAScript具備相似於微型任務的「任務」概念,可是除了模糊的郵件列表討論以外,這種關係並無明確。可是,廣泛的共識是,應將諾言做爲微任務隊列的一部分,這是有充分理由的。
將promise視爲任務會致使性能問題,由於回調可能會因與任務相關的事情(例如渲染)而沒必要要地延遲。因爲與其餘任務源的交互,它還會致使不肯定性,而且可能中斷與其餘API的交互,但稍後會介紹更多。
這是用於使用微任務進行承諾的Edge憑單。WebKit每晚都在作正確的事,所以我認爲Safari最終會解決此問題,而且它彷佛已在Firefox 43中獲得修復。
真正有趣的是,Safari和Firefox都在此發生了迴歸,此問題已獲得修復。我想知道這是否只是一個巧合。
測試是一種方法。查看日誌什麼時候相對於promise&出現setTimeout
,儘管您依靠的是正確的實現。
肯定的方法是查找規格。例如,ref="html.spec.whatwg.org/mu">步驟14setTimeout將任務排隊,而將變異記錄排隊的步驟5將微任務排隊。
如前所述,在ECMAScript領域中,他們稱微任務爲「工做」。在的f="ecma-international.org/">步驟8.a中PerformPromiseThen,EnqueueJob
調用將微任務排隊。
如今,讓咱們看一個更復雜的例子。切向有關學徒, 「不,他們還沒準備好!」。別理他,你準備好了。咱們開工吧…
首先
<div class="outer">
<div class="inner"></div>
</div>
javascript
// 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);
調度「點擊」事件是一項任務。變異觀察者和promise回調做爲微任務排隊。該setTimeout
回調排隊的任務。因此這是怎麼回事:
// 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,
});
所以,Chrome能夠正確處理。「對我來講是新消息」的一點是,微任務是在回調以後處理的(只要沒有其餘JavaScript在中間執行),我認爲它僅限於任務結束。此規則來自HTML規範,用於調用回調:
若是腳本設置對象堆棧如今爲空,請執行微任務檢查點
— HTML:在回調步驟3 以後進行清理
…而且微任務檢查點涉及遍歷微任務隊列,除非咱們已經在處理微任務隊列。相似地,ECMAScript對此做業說:
僅當沒有正在運行的執行上下文而且執行上下文堆棧爲空時才能夠啓動做業的執行。
— ECMAScript:做業和做業隊列
…儘管在HTML上下文中,「能夠存在」變爲「必須存在」。
Firefox和Safari正確耗盡了點擊偵聽器之間的微任務隊列,如突變回調所示,但承諾的排隊彷佛不一樣。鑑於工做和微任務之間的聯繫模糊,這是能夠原諒的,但我仍然但願它們在偵聽器回調之間執行。Firefox票證。野生動物園門票。
使用Edge,咱們已經看到它的隊列承諾不正確,可是它也沒法耗盡點擊偵聽器之間的微任務隊列,相反,它是在調用全部偵聽器以後執行的,這mutate
在兩個click
日誌以後佔單個日誌。錯誤票。
使用上面的相同示例,若是執行如下命令會發生什麼:
inner.click()
這將像之前同樣開始事件調度,可是使用腳本而不是真正的交互。
Why is it different?
// 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,
});
因此,正確的順序是:click
,click
,promise
,mutate
,promise
,timeout
,timeout
,所以Chrome彷佛獲得正確的。
在調用每一個偵聽器回調以後……
若是腳本設置對象堆棧如今爲空,請執行微任務檢查點
— HTML:在回調步驟3 以後進行清理
之前,這意味着微任務在偵聽器回調之間運行,但.click()
會致使事件同步分派,所以調用的腳本.click()
仍在回調之間的堆棧中。上面的規則確保微任務不會中斷執行中的JavaScript。這意味着咱們不處理偵聽器回調之間的微任務隊列,而是在兩個偵聽器以後進行處理。
是的,它會在不起眼的地方(哎呀)咬你。我在嘗試爲使用Promise而非怪異IDBRequest
對象的IndexedDB建立簡單包裝庫時遇到了此問題。它 href="github.com/jakearchibal">幾乎使IDB使用起來頗有趣。
當IDB觸發成功事件時,相關的事務對象在分派後變爲非活動狀態(步驟4)。若是我建立了一個在事件觸發時解決的Promise,則回調應在事務仍處於活動狀態時在第4步以前運行,可是在Chrome之外的其餘瀏覽器中不會發生,這會使庫有點用。
實際上,您能夠在Firefox中解決此問題,由於諸如es6-promise之類的承諾填充將突變觀察者用於回調,而回調正確地使用了微任務。Safari彷佛因該修復程序而遭受競爭條件的折磨,但這可能只是IDB的無效實現。不幸的是,在IE / Edge中事情老是失敗的,由於在回調以後沒法處理突變事件。
但願咱們很快會在這裏開始看到一些互操做性。
綜上所述:
任務按順序執行,瀏覽器能夠在它們之間進行渲染
微任務按順序執行,並執行:
在每次回調以後,只要沒有其餘JavaScript在執行中間
在每一個任務結束時
但願您如今瞭解事件循環的方式,或者至少有藉口能夠躺下。