JS 事件循環機制 - 任務隊列、web API、JS主線程的相互協同


一、JS單線程、異步、同步概念

  從上一篇說明vue nextTick的文章中,多次出現“事件循環”這個名詞,簡單說明了事件循環的步驟,以便理解nextTick的運行時機,這篇文章將更為詳細的分析下事件循環。在此之前需要了解JS單線程,及由此產生的同步執行環境和異步執行環境。
  眾所周知,JS是單線程(如果一個線程刪DOM,一個線程增DOM,瀏覽器傻逼了~所以只能單着了),雖然有webworker醬紫的多線程出現,但也是在主線程的控制下。webworker僅僅能進行計算任務,不能操作DOM,所以本質上還是單線程。
  單線程即任務是串行的,后一個任務需要等待前一個任務的執行,這就可能出現長時間的等待。但由於類似ajax網絡請求、setTimeout時間延遲、DOM事件的用戶交互等,這些任務並不消耗 CPU,是一種空等,資源浪費,因此出現了異步。通過將任務交給相應的異步模塊去處理,主線程的效率大大提升,可以並行的去處理其他的操作。當異步處理完成,主線程空閑時,主線程讀取相應的callback,進行后續的操作,最大程度的利用CPU。此時出現了同步執行和異步執行的概念,同步執行是主線程按照順序,串行執行任務;異步執行就是cpu跳過等待,先處理后續的任務(CPU與網絡模塊、timer等並行進行任務)。由此產生了任務隊列與事件循環,來協調主線程與異步模塊之間的工作。
 

二、事件循環機制

                                  事件循環示例圖
 
  如上圖為事件循環示例圖(或JS運行機制圖),流程如下:
    step1:主線程讀取JS代碼,此時為同步環境,形成相應的堆和執行棧;
    step2:  主線程遇到異步任務,指給對應的異步進程進行處理(WEB API);
    step3:  異步進程處理完畢(Ajax返回、DOM事件處罰、Timer到等),將相應的異步任務推入任務隊列;
    step4: 主線程執行完畢,查詢任務隊列,如果存在任務,則取出一個任務推入主線程處理(先進先出);
    step5: 重復執行step2、3、4;稱為事件循環。
  執行的大意:
    同步環境執行(step1) -> 事件循環1(step4) -> 事件循環2(step4的重復)…
  其中的異步進程有:
    a、類似onclick等,由瀏覽器內核的DOM binding模塊處理,事件觸發時,回調函數添加到任務隊列中;
    b、setTimeout等,由瀏覽器內核的Timer模塊處理,時間到達時,回調函數添加到任務隊列中;
    c、Ajax,由瀏覽器內核的Network模塊處理,網絡請求返回后,添加到任務隊列中。
 

三、任務隊列

  如上示意圖,任務隊列存在多個,同一任務隊列內,按隊列順序被主線程取走;不同任務隊列之間,存在着優先級,優先級高的優先獲取(如用戶I/O);
   3.1、任務隊列的類型
    任務隊列存在兩種類型,一種為microtask queue,另一種為macrotask queue。
    圖中所列出的任務隊列均為macrotask queue,而ES6 的 promise[ECMAScript標准]產生的任務隊列為microtask queue。
            
   3.2、兩者的區別
    microtask queue:唯一,整個事件循環當中,僅存在一個;執行為同步,同一個事件循環中的microtask會按隊列順序,串行執行完畢;
    macrotask queue:不唯一,存在一定的優先級(用戶I/O部分優先級更高);異步執行,同一事件循環中,只執行一個。
 
   3.3、更完整的事件循環流程    
    將microtask加入到JS運行機制流程中,則:
      step1、2、3同上,
      step4:主線程查詢任務隊列,執行microtask queue,將其按序執行,全部執行完畢;
      step5:主線程查詢任務隊列,執行macrotask queue,取隊首任務執行,執行完畢;
      step6:重復step4、step5。
    microtask queue中的所有callback處在同一個事件循環中,而macrotask queue中的callback有自己的事件循環。
    簡而言之:同步環境執行 -> 事件循環1(microtask queue的All)-> 事件循環2(macrotask queue中的一個) -> 事件循環1(microtask queue的All)-> 事件循環2(macrotask queue中的一個)...
    利用microtask queue可以形成一個同步執行的環境,但如果Microtask queue太長,將導致Macrotask任務長時間執行不了,最終導致用戶I/O無響應等,所以使用需慎重。
 

四、示例、驗證  

            console.log('1, time = ' + new Date().toString())
            setTimeout(macroCallback, 0);
            new Promise(function(resolve, reject) {
                console.log('2, time = ' + new Date().toString())
                resolve();
                console.log('3, time = ' + new Date().toString())
            }).then(microCallback);

            function macroCallback() {
                console.log('4, time = ' + new Date().toString())
            } 

            function microCallback() {
                console.log('5, time = ' + new Date().toString())
            }     

  結合第二節與第三節的分析,此處的執行流程應為:

    同步環境:1 -> 2 -> 3

    事件循環1(microCallback):5

    事件循環2(macroCallback):4

  運行結果如下:

    

  運行結果與預期一致,驗證了在不同類型的任務隊列中,microtask queue中的callball將優先執行。

    總結:由此我們了解事件循環的機制,同時了解了任務隊列、JS主線程、異步操作之間的相互協作;同時認識了兩種任務隊列:macrotask queue、microtask queue,它們由不同的標准制定,microtask queue對應ECMAScript的promise屬性(ES6)和 DOM3的MutationObserver,文中說明了兩者在事件循環中的運行情況及區別;在今后的異步操作中,通過靈活運用不同的任務隊列,提升用戶交互性能,給出更加的響應和視覺體驗;同時,通過JS的事件循環機制,可以更清楚JS代碼的執行流,從而更好的控制代碼,更有效、更好的為業務服務。 


免責聲明!

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



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