事件輪詢中的task與microtask


event loop

  網上看到的一篇文章,關於介紹task和Tasks, microtasks, queues and schedules,嘗試簡單翻譯一下寫進來吧!

  原文地址:https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

 

  當我跟我同事Matt Gaunt講,我要寫一篇關於microtask和瀏覽器事件輪詢的文章的時候,他說:“你盡管寫,反正我不看。”好吧,不看就算了,但我還是要寫,總有人會看的。

  事實上, Philip Roberts已經對這方面的知識做了一個很完整的介紹,盡管沒有包含microtasks,但是其他的基本上都有。好了,我要開始我的表演了!

  考慮下面的代碼:

    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 and desktop Safari 8.0.8中,setTimeout可能會在promise1和promise2之前打印-看起來就像是在競爭。看起來很奇怪,一般都是正確打印的。

 

這是為啥呢?

  想要理解這個,必須先了解事件輪詢中的tasks與microtasks。這里面包含不少知識,第一次接觸這個可能會讓你腦闊疼,請深呼吸:

  每一個‘線程’都有它獨立的事件輪詢,所以每個頁面都可以各自工作,執行它們自己的代碼。所有一個來源的窗口都共享同一個事件輪詢,彼此之間同步交流信息。事件輪詢不斷的運轉,執行所有的任務隊列。一個事件輪詢中的任務可能來源於多個地方,需要保證所有任務按正確的順序執行並不簡單,但是瀏覽器會幫忙選擇如何執行這些任務。這樣一來,瀏覽器可以對一些影響性能的操作(如用戶輸入)做特殊處理。跟上!

  Tasks已經被提前排好序,保證了瀏覽器可以持續從內部取出它們並弄到JS/DOM中執行。在兩個任務的執行空隙,瀏覽器可能會重新渲染視圖。在解析HTML頁面的時候,鼠標點擊事件與對應回調函數會產生一個新的task,同時會產生事件序列的還有上面的例子:setTimeout。

  setTimeout會延遲指定的時間,然后將回調函數加入任務序列中。這就是為什么setTimeout會在script end后面打印。script end的打印屬於第一個任務序列的一部分,而setTimeout則在下一個任務序列中被打印。OK,這里基本上沒問題了,希望下一個環節你還能堅持住……

  Microtasks通常在JS當前主任務執行完后直接執行,比如說對一些特殊事件作出響應,或者在不影響主線程情況下異步執行某些事件。一旦沒有其他JS代碼在執行中,microtask隊列會立即執行,執行過程中如果有microtask插入,也同時會被執行。microtas包括mutation observer回調,與上面例子中的promise回調。

  一旦一個promise被決議,在決議后就會形成一個microtask來響應回調函數。這個可以保證promise的即使被決議,回調函數也會被異步執行。因此,調用then(rel,rej)方法后會立即生成一個microtask隊列。這也就是為什么promise1和promise2在script end后面打印,microtask必須在當前JS代碼運轉完后才會被操作。promise1和promise2在setTimeout之前打印,也就是microtask永遠在下一個task之前執行。

  這樣上面的例子就很清晰了:

    //執行JS主代碼
    console.log('script start'); //等待下一輪task
    setTimeout(function() { console.log('setTimeout'); }, 0); //then方法產生microtask
    Promise.resolve().then(function() { console.log('promise1'); //又插入一個microtask 立即執行
        //執行完后進行下一輪task
    }).then(function() { console.log('promise2'); }); //JS主代碼 第一輪task執行完會執行microtask
    console.log('script end');

  如同注釋所說的那樣,一步一步得到了最后的結果。

 

為什么不同瀏覽器會出現差異?

  有些瀏覽器會打印script start,start end,setTimeout,promise1,promise2。promise的回調函數在setTimeout之后執行,看起來似乎將promise當成下一輪task而不是microtask。

  某種程度上可以理解這件事,promise來自於ECMA標准而不是HTML。ECMA標准中有'jobs'的概念,跟microtasks很相似,然而,僅僅通過一些類似郵件的討論,這兩者的區別並不是那么清晰。但是一般來說,都公認promise應該是microtask的一部分,而且確實比較好。

  promise一般用來解決性能問題,有些回調函數可能會因為渲染之類的事件導致延遲執行。(后面的沒太看懂)

 

如何判定這是一個task還是一個microtask

  測試是一種方式。看看setTimeout和promise的打印順序,當然保證結果是正確。

  比較穩妥的方式是看說明文檔。(舉的例子會跳轉到一個新頁面)

  稍微提一下,在ECMA標准中,microtask被叫做‘jobs’。在step 8.a of PerformPromiseThen中,排入隊列被稱為生成一個microtask序列。

  現在來看一個更加復雜的案例:

Lv1 BOSS

  寫這部分之前,我先給點易錯的案例。

  看看下面這一段html:

    <div class="outer">
        <div class="inner"></div>
    </div>

  JS代碼如下:

    var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); new MutationObserver(function() { console.log('mutate'); }).observe(outer, { attributes: true }); function onClick() { console.log('click'); setTimeout(function() { console.log('timeout'); }, 0); Promise.resolve().then(function() { console.log('promise'); }); outer.setAttribute('data-random', Math.random()); } inner.addEventListener('click', onClick); outer.addEventListener('click', onClick);

  如果點擊div.inner,會打印出什么呢?

    在查看答案之前自己分析一下。(提示:有些東西會不止打印一次)

  答案不一樣?或許你是對的,因為不同瀏覽器打印的不一樣。

  Chrome:click,promise,mutate,click,promise,mutate,timeoue,timeoue

  Firefox:click,mutate,click,mutate,timeoue,promise,promise,timeoue

  Safari:click,mutate,click,mutate,promise,promise,timeout,timeout

  IE:click,click,mutate,timeout,promise,timeout,promise

 

哪一個是對的?

  觸發的click事件是一個task。Mutation observer和promise的回調函數是microtask。setTimeout是另外一個task。所以順序這樣是這樣的;

    new MutationObserver(function() { //緊跟在promise后面的microtask
        console.log('mutate'); }).observe(outer, { attributes: true }); function onClick() { //click第一個task
        console.log('click'); //第二個task
        setTimeout(function() { console.log('timeout'); }, 0); //promise產生一個microtask
        Promise.resolve().then(function() { console.log('promise'); }); //這句代碼也會產生一個microtask
        outer.setAttribute('data-random', Math.random()); }

  過程大概是這樣的:點擊div.inner,click(第一個task)->timeout(第二個task)->promise(microtask)->mutate(microtask)。

  按照之前所描述的順序:task->microtask->task,可以得到click,promise,mutate,timeout。但是由於冒泡的關系,外層div也會觸發一遍上面的流程,所以最終結果是click,promise,mutate,click,promise,mutae,timeout,timeout。

  因此,Chrome是正確的。有一個地方對我來說很新鮮,microtask的回調會在沒有其余運行中JS代碼后執行,我理解為task的尾部。下面是HTML文檔中對回調的說明:

  如果棧中JS環境對象為空,會執行microtask隊列的檢查。  

                          —HTML:Cleaning up after a callback

  microtask的檢查包含:遍歷microtask隊列直到全部被執行。

  ECMA標准把這個稱為jobs:

  只有當前環境沒有任何東西在執行並且執行環境棧為空,job才能開始被執行。

                          —ECMAScript:Jobs and Job Queues

  在HTML環境中,'能'變成了'必須'。

 

為什么瀏覽器會出錯?

  Firefox和Safari可以正確的區別microtask與click事件,比如mutation的回調函數,但是promise的處理不太一樣。這個順序會出現問題也是情有可原的,因為關於job和microtask之間區別非常模糊,我認為這兩個在事件回調之間執行比較合理。 Firefox ticketSafari ticket(這里是兩個相關bug討論鏈接)

  至於Edge,它對promise的處理錯的一塌糊塗,同時也未在兩個監聽事件之間執行microtask隊列,等監聽事件都完事了才調用microtask,並且兩個click事件只打印了一次mutate。Bug ticket

 

Lv1 BOSS憤怒的哥哥 

  代碼跟上面的一樣,但是執行的代碼變成了:

    inner.click();

  這個也會觸發同樣的事件,但是方式不是通過點擊,而是直接用JS代碼執行。

  答案如下:

  Chrome:click,click,promise,mutate,promise,timeout,timeout

  Firefox:click,click,mutate,timeout,promise,promise,timeout

  Safari:click,click,muate,promise,promise,timeout,timeout

  IE:click,click,mutate,timeout,promise,timeout,promise

  Chrome每次都會出現不同的結果,我專門弄了一個表來記錄我測試出來的錯誤。如果你在Chrome中得到不一樣的結果,在評論中告訴我版本號。

 

為啥不一樣?

  來梳理一下流程。

  首先這里有一個不一樣的地方,即之前提到的:這里是執行JS代碼觸發函數,不是事件觸發。所以這里的順序是task(執行JS代碼)->task(onClick函數)->打印click->timeout(第二個task)->promise(microtask)->mutate(microtask)->打印click(冒泡)->timeout(第三個task)->promise(microtask),mutate只會觸發一次(不太懂原理),主要區別在於在冒泡的時候,JS代碼仍在執行,所以說microtask不會執行,必須等到第二個click打印才會觸發。最后正確的結果是click,click,promise,mutate,promise,timeout,timeout,看起來Chrome又對了。

  microtask在兩個事件監聽觸發后被調用。

  之前,microtask在監聽回調之間執行,但是通過JS代碼的函數調用,導致事件同步執行了,第一個回調結束后,JS主線程依舊在棧中。上述規則保證了microtask不打斷JS主線程執行。這意味着這種情況下,microtask不能在監聽回調之間執行,而需要在之后。

 

這沒問題嗎?

  可能你會在一些地方還存在疑惑。我曾經在試圖創建a simple wrapper library for IndexedDB that uses promises遇到過這個問題,它比IDBRequest對象還要奇怪。這同時也讓IDB變得好玩起來。almost makes IDB fun to use.

  當IDB成功執行一個事件,相關的事務對象變得沒那么活躍了(transaction object becomes inactive after dispatching沒看懂)。如果在事件執行期間創建了一個promise,該回調會在step4(?)之前執行,此時相關事件仍然在執行,這個現象只會在Chrome中出現,對渲染庫有一點沒用。

  在Firefox中你可以變通解決這個問題,因為promise的polyfill是用mutation observers實現的,正確的實現了microtask。Safari對這兩個microtask一直處於糾結狀態。不幸的是,IE/Edge中也會有問題,mutation不會在回調后執行。

  希望能從這些問題中找到一些共同點。

 

  總結一下:

  ·  task按順序執行,瀏覽器可能在周期間隙里渲染視圖

  ·  microtask也是按順序執行,遵循下列規則:

    1.JS主線程沒有程序執行

    2.主程序的尾部

  希望現在你能明白關於事件輪詢的相關概念,至少會分析執行順序。

  有人在看么。。。(作者原話)

  鳴謝A,B,C……

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM