瀏覽器事件迴圈Event Loop

来源:https://www.cnblogs.com/beckyyyy/archive/2023/11/14/17831297.html
-Advertisement-
Play Games

事件迴圈不是瀏覽器獨有的,從字面上看,“迴圈”可以簡單地認為就是重覆,比如for迴圈,就是重覆地執行for迴圈體中的語句,所以事件迴圈,可以理解為重覆地處理事件,那麼下一個問題是,處理的是什麼事件,事件的相關信息從哪裡獲取。 ...


引言:

事件迴圈不是瀏覽器獨有的,從字面上看,“迴圈”可以簡單地認為就是重覆,比如for迴圈,就是重覆地執行for迴圈體中的語句,所以事件迴圈,可以理解為重覆地處理事件,那麼下一個問題是,處理的是什麼事件,事件的相關信息從哪裡獲取。

因為我沒有用nodejs做過什麼項目,所以這裡我暫且只關註瀏覽器的事件迴圈,但我想就“事件迴圈”本身而言,原理應該是相同的,不過就具體的實現可能存在一些差異。

一道面試題

相信應該有部分小伙伴和我一樣,在面試中曾遇到過類似於這種問列印結果的題目。

(async function main() {
  console.log(1);

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

  setTimeout(() => {
    console.log(3);
  }, 100);

  let p1 = new Promise((resolve, reject) => {
    console.log(4);

    resolve(5);
    console.log(6);
  });

  p1.then((res) => {
    console.log(res);
  });

  let result = await Promise.resolve(7);
  console.log(result);

  console.log(8);
})()

這種題目就是變相的在考察事件迴圈的知識。

我個人感覺事件迴圈這個點,也是隨著Promise的出現,成為了一個常見的考點。

什麼是事件迴圈

一提到事件迴圈,我想很多人會和我一樣,立刻想到非同步、巨集任務、微任務什麼的。

WIKI

先不著急,我們先看下Wiki上,對事件迴圈的通用性描述。

In computer science, the event loop is a programming construct or design pattern that waits for and dispatches events or messages in a program. The event loop works by making a request to some internal or external "event provider" (that generally blocks the request until an event has arrived), then calls the relevant event handler ("dispatches the event"). The event loop is also sometimes referred to as the message dispatcher, message loop, message pump, or run loop.

The event-loop may be used in conjunction with a reactor, if the event provider follows the file interface, which can be selected or 'polled' (the Unix system call, not actual polling). The event loop almost always operates asynchronously with the message originator.

When the event loop forms the central control flow construct of a program, as it often does, it may be termed the main loop or main event loop. This title is appropriate, because such an event loop is at the highest level of control within the program.

簡而言之,事件迴圈是一種編程結構或設計模式,用於在程式中等待和派發事件或消息。

它的工作原理是,向內部或外部的“事件提供者”發出請求(通常會阻止請求,直到事件發生)這就回答了我們之前的問題:事件的信息從哪裡來,是由“事件提供者”提供,然後調用相關的事件處理程式(“派發事件”)關於如何處理事件

事件迴圈有時也被稱為消息派發器、消息迴圈、消息泵或者運行迴圈。

事件迴圈幾乎總是與消息發送者非同步運行

這裡我覺得可以這麼理解,“消息發送者”這邊將事件的消息交給了“事件提供者”,而事件迴圈這邊會向“事件提供者”發出請求獲取事件,然後調用相關的事件處理程式;所以說,事件迴圈與消息發送者是非同步運行。

事件迴圈必然是在“消息發送者”將事件的消息交出之後,才會去執行事件處理程式;也就是說,事件迴圈的操作是在當下之後,在”將來“才會發生的。

當事件迴圈構成程式的中心控制流結構時(通常如此),它可以被稱為主迴圈或主事件迴圈。這個稱謂是恰當的,因為這樣的事件迴圈處於程式的最高控制層。

MDN

WIKI上提供的是通用性的描述。我們再看一下MDN,MDN上直接搜索事件迴圈,可以看到是位於JavaScript路徑下,針對JavaScript事件迴圈的描述。

JavaScript has a runtime model based on an event loop, which is responsible for executing the code, collecting and processing events, and executing queued sub-tasks. This model is quite different from models in other languages like C and Java.

第一段很直白的描述:JavaScript的運行時模型,是基於事件迴圈的,負責執行代碼、收集和處理事件以及執行隊列中的子任務。

執行隊列中的子任務:這基本上等於說,JavaScript的運行時給JavaScript提供了事件迴圈的能力;可以說JavaScript運行時中事件迴圈的部分,提供了JavaScript非同步的具體實現方式。給JavaScript提供了支持非同步的能力。

那麼JavaScript為什麼要處理非同步呢?這就不得不提JavaScript的單線程運行特性 ,線程是什麼?是進行運算調度的最小單位,而JavaScript設計之初,是為了處理網頁上的交互事件,如果JavaScript允許多線程,也就是允許多個觸發的事件同時進行運算,這可能就會呈現出各種不一樣的計算結果,在用戶看來就會顯得交互很混亂,為了減少不確定性,JavaScript乾脆就選擇了單線程運行,所有代碼都在同一個線程中執行;另外,JavaScript中的交互事件很多,如果每個觸發事件都單獨開闢線程來處理,也是不小的開銷吧。

但是呢,雖然JavaScript是單線程運行的,但也存在需要在將來完成的操作,也就是存在非同步代碼,比如定時器。如果在Java中,我們也許可以選擇new一個線程,sleep多少秒,然後再執行,但是JavaScript中不能這樣做,因為它沒有多線程,而如果直接在主線程等待,必定會引發阻塞和卡頓。事件迴圈就是對這種情況的一種解決方案,為了協調瀏覽器中的各種事件,必須使用事件迴圈;而事件迴圈中的消息隊列就由JavaScript運行時來管理。

運行時概念

相信不少前端同學都聽過“運行時”這個詞,那運行時到底是什麼呢?我覺得可以這麼簡單理解,既然運行時的功能是負責執行代碼、收集和處理事件以及執行隊列中的子任務,那麼運行時中必須定義一套規則,關於如何去處理這些事情。所以可以簡單地把運行時認為是定義了一套執行規則的JavaScript執行環境。

關於運行時,可以看到MDN上有一個直觀演示的圖,其中包含了函數調用形成的執行棧、分配對象的堆,以及消息隊列。

根據WIKI給出的描述,運行時模型中,與事件迴圈關係最密切的,是消息隊列,也就是我們前面提到的“事件提供者”。現在我們來看這個隊列。

A JavaScript runtime uses a message queue, which is a list of messages to be processed. Each message has an associated function that gets called to handle the message.

At some point during the event loop, the runtime starts handling the messages on the queue, starting with the oldest one. To do so, the message is removed from the queue and its corresponding function is called with the message as an input parameter. As always, calling a function creates a new stack frame for that function's use.

The processing of functions continues until the stack is once again empty. Then, the event loop will process the next message in the queue (if there is one).

我們來看翻譯的內容:

JavaScript 運行時使用消息隊列,這是一個待處理消息列表。每條消息都有一個相關函數被調用來處理該消息。

在事件迴圈中的某個時刻,運行時開始處理隊列中的消息,從最舊的消息開始。(”隊“這個數據結構我們知道,是先進先出的,所以先進隊的消息會先被處理。)為此,會從隊列中移除消息,並將消息作為輸入參數調用相應的函數。一如既往,調用函數會創建一個新的堆棧框架供該函數使用。

函數的處理將一直持續到堆棧再次清空為止。然後,事件迴圈將處理隊列中的下一條消息(如果有的話)。(也就是,消息隊列中的消息是一條接一條處理的。這裡的堆棧指的就是函數調用形成的執行棧和分配對象的堆)

那麼隊列中的消息是哪裡來的呢? 從這段內容中我們可以知道,進隊的消息已經在等待處理了;所以比如有個定時器setTimeout,定義了有段代碼需要等待3秒才執行,那這段代碼就不能直接就進隊,為了保證動作3秒後才執行,會在3秒後才進隊,也就是說,setTimeout的第二個參數代表的是將消息推入隊列的延遲時間。

那麼肯定需要有什麼東西,來管理這段代碼,將這段代碼在給定的延時後,推入消息隊列。既然js沒法去開線程管理,所以也是瀏覽器在管理;Chrome就有一個定時器線程,專門用於處理定時器,在定時器計時結束後,通知事件觸發線程將消息推入隊列;同樣的,在用戶觸發交互事件時,事件觸發線程也會將已在代碼中定義的消息推入隊列,也就是在事件監聽程式addEventListener中監聽的操作;還有非同步HTTP請求線程,來管理請求回調的消息入隊。等等,瀏覽器的這些線程共同作用來實現事件迴圈這個機制。

在JS主線程空閑時,就會將這些消息隊列中的消息出列,交由主線程來執行。

那麼接下來就是事件迴圈的執行步驟的問題。

事件迴圈執行步驟

首先,關於微任務:我們來看HTML的文檔

Each event loop has a microtask queue, which is a queue of microtasks, initially empty. A microtask is a colloquial way of referring to a task that was created via the queue a microtask algorithm.

每個事件迴圈都有一個微任務隊列,這是一個初始為空的微任務隊列。微任務是一種通俗的說法,指通過微任務隊列演算法創建的任務。

也就是說,每個事件迴圈都會維護一個自己的微任務隊列。它和我們之前看的消息隊列,不是同一個隊列,消息隊列指的是這個文檔中的任務隊列,也就是task queue。

總所周知,常見的產生巨集任務的方式有script、setTimeout、setInterval、UI事件等等;常見的產生微任務的方式有Promise.prototype.then、MutationObserver等等。

假設我們在瀏覽器中載入了一個頁面,現在我們來看事件迴圈的處理步驟

  • 初始狀態:運行時的調用棧空。微任務隊列空,消息隊列里有且僅有一個script腳本(整體代碼)
  • 然後消息隊列中的script腳本被推入調用棧,同步代碼開始執行。
  • 當碰到微任務時,比如Promise.then,就將微任務推入事件迴圈的微任務隊列中;這裡要註意一下,Promise執行器函數中的代碼屬於同步代碼,會被順序執行;
  • 當碰到巨集任務時,就將它們丟給相應的瀏覽器線程;
  • 當本次代碼中的同步代碼都執行完畢後,就將微任務隊列中的任務一一處理並出隊;
  • 這樣就完成了一次迴圈;
  • 本次的巨集任務script腳本也被出隊。
  • 此時DOM修改完成,然後瀏覽器會執行渲染操作,更新界面。
  • 如果巨集任務在各自的線程中被處理完畢後,就會被推入消息隊列。
  • 再接著就是當JS主線程空閑後,會去查詢隊列中是否還有任務,開啟新一輪的迴圈。

這個步驟我大概畫了個圖:
image

現在我們照著最開始的面試題進行舉例。

首先,這段代碼是一整個script腳本,其中的同步代碼會首先被按順序執行,

可以看到這個script腳本中有一個async非同步函數,async函數中的同步代碼會首先被執行,所以先會列印1

然後碰到兩個產生巨集任務的setTimeout,丟給定時器線程,為了後面方便講述,這裡分別把它們叫做巨集任務1和巨集任務2;

然後執行promise執行器函數中的同步代碼,列印4和6

接著碰到Promise.then這個微任務,我們給它記為微任務1,將它推入微任務隊列,

然後我們又碰到一個await,await之後的代碼相當於是Promise.then中的代碼,也就是會被推入微任務隊列,我們給它記為微任務2;

到這裡,本次迴圈中的同步代碼都執行完畢了;

接著就是開始把微任務隊列中的微任務取出執行,首先是執行微任務1,列印5

接著執行微任務2,列印7和8

本次事件迴圈就結束了。

等到計時結束,巨集任務1會先被推入消息隊列,在JS主線程空閑,去查詢消息隊列後,代碼就會被執行,會列印2

同理,巨集任務2後面也會被執行,並列印3

這樣我們就完成了這道面試題的解答。

總結

總的來說,事件迴圈就是JS中非同步的具體實現方式,它的實現需要來自宿主環境的支持,比如瀏覽器中的各種線程,運行時中的消息隊列等等。

本文來自博客園,作者:beckyye,轉載請註明原文鏈接:https://www.cnblogs.com/beckyyyy/p/17831297.html


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

-Advertisement-
Play Games
更多相關文章
  • NineData DSQL 是針對多個同異構資料庫系統進行跨庫查詢的功能,當前支持對錶和視圖進行 SELECT 操作。您可以在一個查詢中訪問多個資料庫,獲取分散在各個資料庫中的有用信息,並且將這些信息聚合為一份查詢結果返回,輕鬆實現跨多個庫、多個數據源,乃至跨多個異構數據源的數據查詢。 ...
  • 火山引擎DataTester上線的「集成工作台」功能,能夠將DataTester的能力與企業自身的系統進行打通,減少系統之間的多次跳轉。幫助企業打造專屬AB平臺,滿足企業的個性化訴求,大幅降低企業服務的應用成本並提升用戶使用體驗。該功能可以通過完善的引導,進行一站式的定製、發佈、嵌出,幫助企業打造專... ...
  • 原文地址: Android app的暗黑模式適配實現 - Stars-One的雜貨小窩 很久之前放在草稿箱的一篇簡單筆記,是之前藍奏雲批量下載工具Android版本實現暗黑主題的適配記錄 本文所說的這裡的暗黑主題,應該只支持Android10系統,不過我手頭的Flyme系統(Android9)上測試 ...
  • 在日常的 JavaScript 編碼中,我們經常使用解構語法來提取對象中的屬性。假設我們有一個名為 fetchResult 的對象,代表從介面返回的數據,其中包含一個欄位名為 data。 const fetchResult = { data: null }; 在提取 data 欄位時,為了避免介面未 ...
  • vue 最有代表性質的就是.VUE 的文件,每一個vue文件都是一個組件,那麼vue 組件的編譯過程是什麼樣的呢 Vue 單文件組件 (SFC)和指令 ast 語法樹 一個 Vue 單文件組件 (SFC),通常使用 *.vue 作為文件擴展名,它是一種使用了類似 HTML 語法的自定義文件格式,用於 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 Vue2 將在 2023 年年底停止維護了,但是 Vue2 的代碼卻不會在 2023 年消失,還會越來越多;難以想象幾十萬行或者幾百萬行的 Vue2 代碼遷移到 Vue3,這是不可能辦到的; 老一點的前端程式員肯定經歷過把大型項目從 ...
  • 從一個假死頁面引發的思考: 作為前端開發,除了要攻剋頁面難點,也要有更深的自我目標,性能優化是自我提升中很重要的一環; 在前端開發中,會偶遇到頁面假死的現象, 是因為當js有大量計算時,會造成 UI 阻塞,出現界面卡頓、掉幀等情況,嚴重時會出現頁面卡死的情況; ...
  • 本文介紹使用canvas的drawImage進行視頻截圖,並用toDataURL和toBlob轉化為圖片地址的方法,且重點介紹了將bold信息轉化為圖片地址的方法。 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...