1. 程式人生 > >js事件迴圈機制

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程式碼的執行流,從而更好的控制程式碼,更有效、更好的為業務服務。