深入理解JavaScript的事件迴圈(Event Loop)

来源:https://www.cnblogs.com/imwtr/archive/2018/07/28/9383695.html
-Advertisement-
Play Games

一、什麼是事件迴圈 JS的代碼執行是基於一種事件迴圈的機制,之所以稱作事件迴圈,MDN給出的解釋為 因為它經常被用於類似如下的方式來實現 如果當前沒有任何消息queue.waitForMessage 會等待同步消息到達 我們可以把它當成一種程式結構的模型,處理的方案。更詳細的描述可以查看 這篇文章 ...


 

一、什麼是事件迴圈

JS的代碼執行是基於一種事件迴圈的機制,之所以稱作事件迴圈,MDN給出的解釋為

因為它經常被用於類似如下的方式來實現

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

如果當前沒有任何消息queue.waitForMessage 會等待同步消息到達

我們可以把它當成一種程式結構的模型,處理的方案。更詳細的描述可以查看 這篇文章

而JS的運行環境主要有兩個:瀏覽器Node

在兩個環境下的Event Loop實現是不一樣的,在瀏覽器中基於 規範 來實現,不同瀏覽器可能有小小區別。在Node中基於 libuv 這個庫來實現

 JS是單線程執行的,而基於事件迴圈模型,形成了基本沒有阻塞(除了alert或同步XHR等操作)的狀態

 

 二、Macrotask 與 Microtask

根據 規範,每個線程都有一個事件迴圈(Event Loop),在瀏覽器中除了主要的頁面執行線程 外,Web worker是在一個新的線程中運行的,所以可以將其獨立看待。

每個事件迴圈有至少一個任務隊列(Task Queue,也可以稱作Macrotask巨集任務),各個任務隊列中放置著不同來源(或者不同分類)的任務,可以讓瀏覽器根據自己的實現來進行優先順序排序

以及一個微任務隊列(Microtask Queue),主要用於處理一些狀態的改變,UI渲染工作之前的一些必要操作(可以防止多次無意義的UI渲染)

主線程的代碼執行時,會將執行程式置入執行棧(Stack)中,執行完畢後出棧,另外有個堆空間(Heap),主要用於存儲對象及一些非結構化的數據

一開始

巨集任務與微任務隊列里的任務隨著:任務進棧、出棧、任務出隊、進隊之間交替著進行

從macrotask隊列中取出一個任務處理,處理完成之後(此時執行棧應該是空的),從microtask隊列中一個個按順序取出所有任務進行處理,處理完成之後進入UI渲染後續工作

需要註意的是:microtask並不是在macrotask完成之後才會觸發,在回調函數之後,只要執行棧是空的,就會執行microtask。也就是說,macrotask執行期間,執行棧可能是空的(比如在冒泡事件的處理時)

然後迴圈繼續

常見的macrotask有:

  • run <script>(同步的代碼執行)

  • setTimeout
  • setInterval

  • setImmediate (Node環境中)

  • requestAnimationFrame

  • I/O

  • UI rendering

 

常見的microtask有:

  • process.nextTick (Node環境中)

  • Promise callback

  • Object.observe (基本上已經廢棄)

  • MutationObserver

 

macrotask種類很多,還有 dispatch event事件派發等

run <script>這個可能看起來比較奇怪,可以把它看成一段代碼(針對單個<script>標簽)的同步順序執行,主要用來描述執行程式的第一步執行

dispatch event主要用來描述事件觸發之後的執行任務,比如用戶點擊一個按鈕,觸發的onClick回調函數。需要註意的是,事件的觸發是同步的,這在下文有例子說明

 

註:

當然,也可認為 run <script>不屬於macrotask,畢竟規範也沒有這樣的說明,也可以將其視為主線程上的同步任務,不在主線程上的其他部分為非同步任務

 

三、在瀏覽器中的實現

先來看看這段蠻複雜的代碼,思考一下會輸出什麼

            console.log('start');

            var intervalA = setInterval(() => {
                console.log('intervalA');
            }, 0);

            setTimeout(() => {
                console.log('timeout');

                clearInterval(intervalA);
            }, 0);

            var intervalB = setInterval(() => {
                console.log('intervalB');
            }, 0);

            var intervalC = setInterval(() => {
                console.log('intervalC');
            }, 0);

            new Promise((resolve, reject) => {
                console.log('promise');

                for (var i = 0; i < 10000; ++i) {
                    i === 9999 && resolve();
                }

                console.log('promise after for-loop');
            }).then(() => {
                console.log('promise1');
            }).then(() => {
                console.log('promise2');

                clearInterval(intervalB);
            });

            new Promise((resolve, reject) => {
                setTimeout(() => {
                    console.log('promise in timeout');
                    resolve();
                });

                console.log('promise after timeout');
            }).then(() => {
                console.log('promise4');
            }).then(() => {
                console.log('promise5');

                clearInterval(intervalC);
            });

            Promise.resolve().then(() => {
                console.log('promise3');
            });

            console.log('end');    

上述代碼結合了常規執行代碼,setTimeout,setInterval,Promise 

答案為

 

 在解釋為什麼之前,先看一個更簡單的例子

            console.log('start');

            setTimeout(() => {
                console.log('timeout');
            }, 0);

            Promise.resolve().then(() => {
                console.log('promise');
            });

            console.log('end');    

 大概的步驟,文字有點多

1. 運行時(runtime)識別到log方法為一般的函數方法,將其入棧,然後執行輸出 start 再出棧

2. 識別到setTimeout為特殊的非同步方法(macrotask),將其交由其他內核模塊處理,setTimeout的匿名回調函數被放入macrotask隊列中,並設置了一個 0ms的立即執行標識(提供後續模塊的檢查)

3. 識別到Promise的resolve方法為一般的方法,將其入棧,然後執行 再出棧

4. 識別到then為Promise的非同步方法(microtask),將其交由其他內核模塊處理,匿名回調函數被放入microtask隊列中

5. 識別到log方法為一般的函數方法,將其入棧,然後執行輸出 end 再出棧

6. 主線程執行完畢,棧為空,隨即從microtask隊列中取出隊首的項,

這裡隊首為匿名函數,匿名函數裡面有 console的log方法,也將其入棧(如果執行過程中識別到特殊的方法,就在這時交給其他模塊處理到對應隊列尾部),

輸出 promise後出棧,並將這一項從隊列中移除

7. 繼續檢查microtask隊列,當前隊列為空,則將當前macrotask出隊,進入下一步(如果不為空,就繼續取下一個microtask執行)

8.檢查是否需要進行UI重新渲染等,進行渲染...

9. 進入下一輪事件迴圈,檢查macrotask隊列,取出一項進行處理

 所以最終的結果是

 

再看上面那個例子,對比起來只是代碼多了點,混入了setInterval,多個setTimeout與promise的函數部分,按照上面的思路,應該不難理解

需要註意的三點:

1. clearInterval(intervalA); 運行的時候,實際上已經執行了 intervalA 的macrotask了
2. promise函數內部是同步處理的,不會放到隊列中,放入隊列中的是它的then或catch回調
3. promise的then返回的還是promise,所以在輸出promise4後,繼續檢測到後續的then方法,馬上放到microtask隊列尾部,再繼續取出執行,馬上輸出promise5;

而輸出promise1之後,為什麼沒有馬上輸出promise2呢?因為此時promise1所在任務之後是promise3的任務,1和3在promise函數內部返回後就添加至隊列中,2在1執行之後才添加

 

再來看個例子,就有點微妙了

<script>
        console.log('start');

        setTimeout(() => {
            console.log('timeout1');
        }, 0);

        Promise.resolve().then(() => {
            console.log('promise1');
        });
    </script>
    <script>
        setTimeout(() => {
            console.log('timeout2');
        }, 0);

        requestAnimationFrame(() => {
            console.log('requestAnimationFrame');
        });

        Promise.resolve().then(() => {
            console.log('promise2');
        });

        console.log('end');
    </script>

輸出結果

requestAnimationFrame是在setTimeout之前執行的,start之後並不是直接輸出end,也許這兩個<script>標簽被獨立處理了

 

來看一個關於DOM操作的例子,Tasks, microtasks, queues and schedules

 

<style type="text/css">
    .outer {
        width: 100px;
        background: #eee;
        height: 100px;
        margin-left: 300px;
        margin-top: 150px;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    .inner {
        width: 50px;
        height: 50px;
        background: #ddd;
    }
</style>

<script>
        var outer = document.querySelector('.outer'),
            inner = document.querySelector('.inner'),
            clickTimes = 0;

        new MutationObserver(() => {
            console.log('mutate');
        }).observe(outer, {
            attributes: true
        });

        function onClick() {
            console.log('click');

            setTimeout(() => {
                console.log('timeout');
            }, 0);

            Promise.resolve().then(() => {
                console.log('promise');
            });

            outer.setAttribute('data-click', clickTimes++);
        }

        inner.addEventListener('click', onClick);
        outer.addEventListener('click', onClick);

        // inner.click();

        // console.log('done');
    </script>

點擊內部的inner塊,會輸出什麼呢?

MutationObserver優先順序比promise高,雖然在一開始就被定義,但實際上是觸發之後才會被添加到microtask隊列中,所以先輸出了promise

兩個timeout回調都在最後才觸發,因為click事件冒泡了,事件派發這個macrotask任務包括了前後兩個onClick回調,兩個回調函數都執行完之後,才會執行接下來的 setTimeout任務

期間第一個onClick回調完成後執行棧為空,就馬上接著執行microtask隊列中的任務

 

如果把代碼的註釋去掉,使用代碼自動 click(),思考一下,會輸出什麼?

可以看到,事件處理是同步的,done在連續輸出兩個click之後才輸出

 而mutate只有一個,是因為當前執行第二個onClick回調的時候,microtask隊列中已經有一個MutationObserver,它是第一個回調的,因為事件同步的原因沒有被及時執行。瀏覽器會對MutationObserver進行優化,不會重覆添加監聽回調

 

 

 四、在Node中的實現

在Node環境中,macrotask部分主要多了setImmediate,microtask部分主要多了process.nextTick,而這個nextTick是獨立出來自成隊列的,優先順序高於其他microtask

不過事件迴圈的的實現就不太一樣了,可以參考 Node事件文檔   libuv事件文檔

Node中的事件迴圈有6個階段

  • timers:執行setTimeout() 和 setInterval()中到期的callback
  • I/O callbacks:上一輪迴圈中有少數的I/Ocallback會被延遲到這一輪的這一階段執行
  • idle, prepare:僅內部使用
  • poll:最為重要的階段,執行I/O callback,在適當的條件下會阻塞在這個階段
  • check:執行setImmediate的callback
  • close callbacks:執行close事件的callback,例如socket.on("close",func)

每一輪事件迴圈都會經過六個階段,在每個階段後,都會執行microtask

 

比較特殊的是在poll階段,執行程式同步執行poll隊列里的回調,直到隊列為空或執行的回調達到系統上限

接下來再檢查有無預設的setImmediate,如果有就轉入check階段,沒有就先查詢最近的timer的距離,以其作為poll階段的阻塞時間,如果timer隊列是空的,它就一直阻塞下去

而nextTick並不在這些階段中執行,它在每個階段之後都會執行

 

看一個例子

setTimeout(() => console.log(1));

setImmediate(() => console.log(2));

process.nextTick(() => console.log(3));

Promise.resolve().then(() => console.log(4));

console.log(5);

根據以上知識,應該很快就能知道輸出結果是 5 3 4 1 2

修改一下

process.nextTick(() => console.log(1));

Promise.resolve().then(() => console.log(2));

process.nextTick(() => console.log(3));

Promise.resolve().then(() => {
    process.nextTick(() => console.log(0));
    console.log(4);
});

輸出為 1 3 2 4 0,因為nextTick隊列優先順序高於同一輪事件迴圈中其他microtask隊列

修改一下

process.nextTick(() => console.log(1));

console.log(0);

setTimeout(()=> {
    console.log('timer1');

    Promise.resolve().then(() => {
        console.log('promise1');
    });
}, 0);

process.nextTick(() => console.log(2));

setTimeout(()=> {
    console.log('timer2');

    process.nextTick(() => console.log(3));

    Promise.resolve().then(() => {
        console.log('promise2');
    });
}, 0);

輸出為

與在瀏覽器中不同,這裡promise1並不是在timer1之後輸出,因為在setTimeout執行的時候是出於timer階段,會先一併處理timer回調

 

setTimeout是優先於setImmediate的,但接下來這個例子卻不一定是先執行setTimeout的回調

 

setTimeout(() => {
    console.log('timeout');
}, 0);

setImmediate(() => {
    console.log('immediate');
});

因為在Node中識別不了0ms的setTimeout,至少也得1ms. 

所以,如果在進入該輪事件迴圈的時候,耗時不到1ms,則setTimeout會被跳過,進入check階段執行setImmediate回調,先輸出 immediate

如果超過1ms,timer階段中就可以馬上處理這個setTimeout回調,先輸出 timeout

修改一下代碼,讀取一個文件讓事件迴圈進入IO文件讀取的poll階段

    let fs = require('fs');

    fs.readFile('./event.html', () => {
        setTimeout(() => {
            console.log('timeout');
        }, 0);

        setImmediate(() => {
            console.log('immediate');
        });
    });

這麼一來,輸出結果肯定就是 先 immediate  後 timeout

 

 五、用好事件迴圈

知道JS的事件迴圈是怎麼樣的了,就需要知道怎麼才能把它用好

1. 在microtask中不要放置複雜的處理程式,防止阻塞UI的渲染

2. 可以使用process.nextTick處理一些比較緊急的事情

3. 可以在setTimeout回調中處理上輪事件迴圈中UI渲染的結果

4. 註意不要濫用setInterval和setTimeout,它們並不是可以保證能夠按時處理的,setInterval甚至還會出現丟幀的情況,可考慮使用 requestAnimationFrame

5. 一些可能會影響到UI的非同步操作,可放在promise回調中處理,防止多一輪事件迴圈導致重覆執行UI的渲染

6. 在Node中使用immediate來可能會得到更多的保證

7. 不要糾結


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1、表和sql的優化 -》大表拆分成小表、分區表、外部表、臨時表都是屬於優化的一塊 -》分區表:檢索更快速 -》外部表:數據安全性 -》臨時表&拆分子表:簡化複雜的SQL以及需求 2、SQL可以從join和fliter兩方面深入 3、MR優化 -》map和reduce的個數 -》一個分片就是一個塊, ...
  • 課程教師:李興華 課程學習者:陽光羅諾 日期:2018-07-28 知識點: 1、 瞭解PL/SQL的主要特點 2、 掌握PL/SQL塊的基本結構 PL/SQL PL/SQL是Oracle在關係資料庫結構化查詢語言SQL基礎上擴展得到的一種過程化查詢語言。 SQL與編程語言之間的不同之處在於,SQL ...
  • 最近很多人都想學習大數據開發,但是卻不知道如何開始學習,今天軟妹子專門整理了一份針對大數據初學者的大數據開發學習路線。 下麵分十個章節來說明大數據開發要學習的內容: 以上就是一個大數據新手,想要學會大數據開發,需要學習的內容,大數據學習是一個持續的過程,只要用心學,沒有學不會的東西哦!我要推薦下我自 ...
  • 一、整體瞭解數據分析新人們被”大數據“、”人工智慧“、”21世紀是數據分析師的時代“等等信息吸引過來,立志成為一名數據分析師,於是問題來了,數據分析到底是乾什麼的?數據分析都包含什麼內容?市面上有很多講數據分析內容的書籍,在此我推薦《深入淺出數據分析》,此書對有基礎人士可稱消遣讀物, 但對新人們還是 ...
  • 占座 ...
  • 本文部分內容來自於網路,點擊瀏覽原文 app:layout_constraintLeft_toLeftOf //Constrains the left side of a child to the left side of a target child (contains the target ch ...
  • 女孩:上海站到了? 男孩:嗯呢?走向世界~ 女孩:Intent核心技術和數據存儲技術? 男孩:對,今日就講這個~ Intent是各個組件之間用來進行通信的,Intent的翻譯為“意圖”的意思,是傳輸數據的核心對象,它可以開啟一個activity,也可以發送廣播消息和開啟Service服務,對於他們之 ...
  • (轉自)https://www.cnblogs.com/robbinluobo/p/7217387.html String、JsonObject、JavaBean 互相轉換 User user = new Gson().fromJson(jsonObject, User.class); User u ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...