詳解JavaScript的任務识埋、微任務、隊列以及代碼執(zhí)行順序

摘要: 理解JS的執(zhí)行順序零渐。

思考下面 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惠豺,但是由于瀏覽器實現(xiàn)支持不同導致結(jié)果也不一致银还。

Microsoft Edge、Firefox 40洁墙、iOS Safari和桌面Safari 8.0.8 打印promise1promise2之前會先打印 setTimeout —— 這似乎是瀏覽器廠商相互競爭導致的實現(xiàn)不同见剩。這真的很奇怪,因為 Firefox 39 和 Safari 8.0.7 結(jié)果總是正確的扫俺。

為什么會這樣

要理解這一點苍苞,需要了解事件循環(huán)<event loop>如何處理任務和微任務。

每個“線程”都有自己的事件循環(huán)<event loop>狼纬,因此每個 web worker 都有自己的事件循環(huán)羹呵,因此可以獨立執(zhí)行,而來自同域的所有窗口共享一個事件循環(huán)疗琉,所以它們可以同步地通信冈欢。

事件循環(huán)持續(xù)運行,直到清空 Tasks 隊列的任務盈简。一個事件循環(huán)有多個任務源凑耻,這些任務源保證了該源中的執(zhí)行順序(比如IndexedDB定義了它們自己的規(guī)范),但是瀏覽器可以在每次循環(huán)中選擇哪個源來執(zhí)行任務柠贤。這允許瀏覽器優(yōu)先選擇性能敏感的任務香浩,比如用戶輸入等。

Tasks 被放到任務源中臼勉,這樣瀏覽器就可以從內(nèi)部進入JavaScript/DOM領(lǐng)域邻吭,并確保這些操作按順序進行。在Tasks 執(zhí)行期間宴霸,瀏覽器可能更新渲染囱晴。從鼠標點擊到事件回調(diào)需要調(diào)度一個任務,解析超文本標記語言也是如此瓢谢。

setTimeout遲給定的時間畸写,然后為它的回調(diào)調(diào)度一個新任務。這就是為什么setTimeout在打印script end之后打印氓扛,因為打印script end是第一個任務的一部分枯芬,而setTimeout在一個單獨的任務中。

微任務<Microtasks>通常是針對當前執(zhí)行腳本之后應該立即發(fā)生的事情進行調(diào)度的幢尚,比如對一批操作進行響應破停,或者在不影響整個新任務的情況下進行異步處理翅楼。

只要沒有其他JavaScript處于執(zhí)行中期尉剩,并且在每個任務的末尾,微任務隊列就在回調(diào)之后處理毅臊。在微任務期間排隊的任何其他微任務都會被添加到隊列的末尾并進行處理理茎。微任務 包括 MutationObserver callbacks。例如上面的例子中的 promisecallback

一個settled狀態(tài)的promise 或者已經(jīng)變成settled狀態(tài)(異步請求被settled)的promise皂林,會立刻將它的callback(then)放到微任務隊列里面朗鸠。

這確保了 promise 回調(diào)是異步的,即便promise已經(jīng)變?yōu)?code>settled狀態(tài)础倍。因此一個已settledpromise調(diào)用.then(yey,nay)時將立即把一個微任務加入微任務隊列中烛占。

這就是為什么promise1promise2會在script end后打印,因為當前運行的腳本必須在處理微任務之前完成沟启。promise1promise2setTimeout之前打印忆家,因為微任務總是在下一個任務之前發(fā)生。

好德迹,一步一步的運行:

image

瀏覽器之間會有什么不同芽卿?

一些瀏覽器的打印的順序是 script start, script end, setTimeout, promise1, promise2。它們在setTimeout之后運行promise回調(diào)胳搞。很可能他們調(diào)用promise回調(diào)是作為新任務的一部分卸例,而不是作為一個微任務。

這也是可以理解的肌毅,因為promise來自 ECMAScript 而不是 HTML筷转。ECMAScript 有“作業(yè)”的概念,類似于微任務悬而,但是除了模糊的郵件列表討論之外旦装,這種關(guān)系并不明確。然而摊滔,普遍的共識是阴绢,promise應該是微任務隊列的一部分并且有充足的理由。

promise 看作任務會導致性能問題艰躺,因為回調(diào)沒有必要因為任務相關(guān)的事(比如渲染)而延遲執(zhí)行呻袭。它還會由于與其他任務源的交互而導致非確定性,并可能中斷與其他api的交互腺兴,稍后將詳細介紹左电。

這里有一條 Edge 反饋,它錯誤地將 promises 當作 任務页响。WebKit nightly 做對了篓足,所以我認為 Safari 最終會修復,而 Firefox 43 似乎已經(jīng)修復闰蚕。

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

動手試一試是一種辦法栈拖,查看相對于promisesetTimeout如何打印,盡管這取決于實現(xiàn)是否正確没陡。

一種方法是查看規(guī)范: 將一個任務加入隊列: step 14 of setTimeout

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

如上所述涩哟,ECMAScript 將微任務稱為作業(yè): 調(diào)用 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);

在偷看答案前先試一試

試一試

image

和你猜想的有不同嗎贴彼?如果是潜腻,你得到的結(jié)果可能也是正確的。不幸的是器仗,瀏覽器實現(xiàn)并不統(tǒng)一融涣,下面是各個瀏覽器下測試結(jié)果:

image

誰是正確的?

調(diào)度'click'事件是一項任務。 Mutation observer 和 promise 回調(diào)被列為微任務精钮。 setTimeout 回調(diào)列為任務暴心。 因此運行過程如下:

image

所以 Chrome 是對的。對我來說新發(fā)現(xiàn)是杂拨,微任務在回調(diào)之后運行(只要沒有其它的 Javascript 在運行)专普,我原以為它只能在一個任務的末尾執(zhí)行。

瀏覽器出了什么問題弹沽?

對于 mutation callbacks檀夹,F(xiàn)irefox 和 Safari 都正確地在內(nèi)部區(qū)域和外部區(qū)域單擊事件之間執(zhí)行完畢,清空了微任務隊列策橘,但是 promises 列隊的處理看起來和chrome不一樣炸渡。這多少情有可原,因為作業(yè)和微任務的關(guān)系不清楚丽已,但是我仍然期望在事件回調(diào)之間處理 Firefox ticket. Safari ticket.

對于 Edge蚌堵,我們已經(jīng)看到它錯誤的將 promises 當作任務,它也沒有在單擊回調(diào)之間清空微任務隊列沛婴,而是在所有單擊回調(diào)執(zhí)行完之后清空吼畏,于是總共只有一個 mutate 在兩個 click 之后打印。

等級一 boss打怪升級

仍然使用上面的例子嘁灯,假如我們運行下面代碼會怎么樣:

inner.click();

跟之前一樣泻蚊,它會觸發(fā) click 事件,但這次是通過 JS 調(diào)用的丑婿。

試一試

image

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

image

我發(fā)誓我一直在從Chrome中得到不同的結(jié)果性雄,我已經(jīng)更新了這張圖表很多次了,我以為我在錯誤地測試Canary羹奉。如果你在Chrome中得到不同的結(jié)果秒旋,請在評論中告訴我是哪個版本。

為什么不同诀拭?

應該是這樣的:

image

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

以前炫加,這意味著微任務在偵聽器回調(diào)之間運行瑰煎,但.click()會導致事件同步調(diào)度铺然,因此調(diào)用.click()的腳本仍然在回調(diào)之間的堆棧中俗孝。 上述規(guī)則確保微任務不會中斷執(zhí)行中期的JavaScript酒甸。 這意味著我們不處理偵聽器回調(diào)之間的微任務隊列,它們在兩個偵聽器之后處理赋铝。

總結(jié)

任務按順序執(zhí)行插勤,瀏覽器可以在它們之間進行渲染:

微任務按順序執(zhí)行,并執(zhí)行:

  • 在每個回調(diào)之后革骨,只要沒有其它代碼正在運行玩裙。
  • 在每個任務的末尾洗搂。

關(guān)于Fundebug

Fundebug專注于JavaScript、微信小程序、微信小游戲枉圃、支付寶小程序、React Native虐块、Node.js和Java線上應用實時BUG監(jiān)控吨艇。 自從2016年雙十一正式上線,F(xiàn)undebug累計處理了10億+錯誤事件巍实,付費客戶有陽光保險滓技、核桃編程、荔枝FM棚潦、掌門1對1令漂、微脈、青團社等眾多品牌企業(yè)丸边。歡迎大家免費試用叠必!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市妹窖,隨后出現(xiàn)的幾起案子挠唆,更是在濱河造成了極大的恐慌,老刑警劉巖嘱吗,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件玄组,死亡現(xiàn)場離奇詭異,居然都是意外死亡谒麦,警方通過查閱死者的電腦和手機俄讹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绕德,“玉大人患膛,你說我怎么就攤上這事〕苌撸” “怎么了踪蹬?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵胞此,是天一觀的道長。 經(jīng)常有香客問我跃捣,道長漱牵,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任疚漆,我火速辦了婚禮酣胀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘娶聘。我一直安慰自己闻镶,他們只是感情好,可當我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布丸升。 她就那樣靜靜地躺著铆农,像睡著了一般。 火紅的嫁衣襯著肌膚如雪狡耻。 梳的紋絲不亂的頭發(fā)上墩剖,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天,我揣著相機與錄音酝豪,去河邊找鬼涛碑。 笑死,一個胖子當著我的面吹牛孵淘,可吹牛的內(nèi)容都是我干的蒲障。 我是一名探鬼主播,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼瘫证,長吁一口氣:“原來是場噩夢啊……” “哼揉阎!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起背捌,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤毙籽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后毡庆,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體坑赡,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年么抗,在試婚紗的時候發(fā)現(xiàn)自己被綠了毅否。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡蝇刀,死狀恐怖螟加,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤捆探,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布然爆,位于F島的核電站,受9級特大地震影響黍图,放射性物質(zhì)發(fā)生泄漏曾雕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一雌隅、第九天 我趴在偏房一處隱蔽的房頂上張望翻默。 院中可真熱鬧缸沃,春花似錦恰起、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至翘单,卻和暖如春吨枉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背哄芜。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工貌亭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人认臊。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓圃庭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親失晴。 傳聞我的和親對象是個殘疾皇子剧腻,可洞房花燭夜當晚...
    茶點故事閱讀 43,562評論 2 349

推薦閱讀更多精彩內(nèi)容