1. 程式人生 > >跟著whatwg看一遍事件迴圈

跟著whatwg看一遍事件迴圈

## 前言 對於單執行緒來說,事件迴圈可以說是重中之重了,它為任務分配不同的優先順序,井然有序的排程。讓js解析,使用者互動,頁面渲染等互不衝突,各司其職。 我們書寫的程式碼無時無刻都在和事件迴圈打交道,要想寫出更流暢,我們就必須深入瞭解事件迴圈,下面我們將從[規範](https://html.spec.whatwg.org/multipage/webappapis.html#event-loops)中翻譯和解讀整個流程。 以下內容來自whatwg文件,均為個人理解,若有不對,煩請指出,我會第一時間修改,避免誤導他人! ## 正文 為了協呼叫戶操作,js執行,頁面渲染,網路請求等事件,每個宿主中,存在[事件迴圈](https://tc39.es/ecma262/#sec-agents)這樣的角色,並且該角色在當前宿主中是唯一的。 > 簡單解釋一下宿主:宿主是一個ECMAScript執行上下文,一般包含執行上下文棧,執行時執行環境,宿主記錄和一個執行執行緒,除了這個執行執行緒外,其他的專屬於當前宿主。例如,某些瀏覽器在不同的tabs使用同一個執行執行緒。 不僅如此,事件迴圈又存於在各個不同場景,有瀏覽器環境下的,worker環境下的和Worklet環境下的。 > [Worklet](https://developer.mozilla.org/en-US/docs/Web/API/Workle)是一個輕量級的web worker,可以讓開發者訪問更底層的渲染工作線,也就是說你可以通過Worklet去幹預瀏覽器的渲染環境。 提到了worklet,那就順便看一個例子(需開啟服務,不要以file協議執行),通過這個例子,可以看到事件迴圈不同階段觸發了什麼鉤子函式: ```html Document

My Cool Header

``` ```javascript // paint.js registerPaint( 'headerHighlight', class { static get contextOptions() { console.log('contextOptions'); return {alpha: true}; } paint(ctx) { console.log('paint函式'); } } ); // ==========================分割線 // layout.js registerLayout( 'sample-layout', class { async intrinsicSizes(children, edges, styleMap) {} async layout(children, edges, constraints, styleMap, breakToken) { console.log('layout階段'); } } ); ``` ![](https://user-gold-cdn.xitu.io/2020/6/24/172e47147c6b2fc5?w=1122&h=692&f=jpeg&s=87673) 事件迴圈有一個或多個Task佇列,每個Task佇列都是Task的一個集合。其中Task不是指我們的某個函式,而是一個上下文環境,結構如下: - step:一系列任務將要執行的步驟 - source:任務來源,常用來對相關任務進行分組和系列化 - document:與當前任務相關的document物件,如果是非window環境則為null - 環境配置物件:在任務期間追蹤記錄任務狀態 >
這裡的Task佇列不是Task,是一個集合,因為取出一個Task佇列中的Task是選擇一個可執行的Task,而不是出隊操作。 > 微任務佇列是一個入對出對的佇列。 這裡說明一下,Task佇列為什麼有多個,因為不同的Task佇列有不同的優先順序,進而進行次序排列和呼叫,有沒有感覺react的fiber和這個有點類似? 舉個例子,Task佇列可以是專門負責滑鼠和鍵盤事件的,並且賦予滑鼠鍵盤佇列較高的優先順序,以便及時響應使用者操作。另一個Task佇列負責其他任務源。不過也不要餓死任何一個task,這個後續處理模型中會介紹。 Task封裝了負責以下任務的演算法: - Events: 由專門的Task在特定的EventTarget(一個具有監聽訂閱模式列表的物件)上分發事件物件 - Parsing: html解析器標記一個或多個位元組,並處理所有生成的結果token - Callbacks: 由專門的Task觸發回撥函式 - Using a resource: 當該演算法獲取資源的時候,如果該階段是以非阻塞方式發生,那麼一旦部分或者全部資源可用,則由Task進行後續處理 - Reacting to DOM manipulation: 通過dom操作觸發的任務,例如插入一個節點到document 事件迴圈有一個當前執行中的Task,可以為null,如果是null的話,代表著可以接受一個新的Task(新一輪的步驟)。 事件迴圈有微任務佇列,預設為空,其中的任務由微任務排隊演算法建立。 事件迴圈有一個執行微任務檢查點,預設為false,用來防止微任務死迴圈。 [微任務排隊演算法](https://html.spec.whatwg.org/multipage/webappapis.html#queuing-tasks): 1. 如果未提供event loop,設定一個隱式event loop。 1. 如果未提供document,設定一個隱式document. 1. 建立一個Task作為新的微任務 1. 設定setp、source、document到新的Task上 1. 設定Task的環境配置物件為空集 1. 新增到event loop的微任務佇列中 微任務檢查演算法: 1. 如果微任務檢查標誌為true,直接return 2. 設定微任務檢查標誌為true 3. 如果微任務隊裡不為空(也就是說微任務新增的微任務也會在這個迴圈中出現,直到微任務佇列為空): 1. 從微任務佇列中找出最老的任務(防餓死) 2. 設定當前執行任務為這個最老的任務 3. 執行 4. 重置當前執行任務為null 4. 通知環境配置物件的promise進行reject操作 5. 清理indexdb事務(不太明白這一步,如果有讀者瞭解,煩請點撥一下) 6. 設定微任務檢查標誌為false #### 處理模型 event loop會按照下面這些步驟進行排程: 1. 找到一個可執行的Task佇列,如果沒有則跳轉到下面的微任務步驟 2. 讓最老的Task作為Task佇列中第一個可執行的Task,並將其移除 3. 將最老的Task作為event loop的可執行Task 4. 記錄任務開始時間點 5. 執行Task中的setp對應的步驟(上文中Task結構中的step) 6. 設定event loop的可執行任務為null 7. 執行微任務檢查演算法 8. 設定hasARenderingOpportunity(是否可以渲染的flag)為false 9. 記住當前時間點 10. 通過下面步驟記錄任務持續時間 1. 設定頂層瀏覽器環境為空 1. 對於每個最老Task的指令碼執行環境配置物件,設定當前的頂級瀏覽器上下文到其上 3. 報告消耗過長的任務,並附帶開始時間,結束時間,頂級瀏覽器上下文和當前Task 11. 如果在window環境下,會根據硬體條件決定是否渲染,比如重新整理率,頁面效能,頁面是否在後臺,不過渲染會定期出現,避免頁面卡頓。值得注意的是,正常的重新整理率為60hz,大概是每秒60幀,大約16.7ms每幀,如果當前瀏覽器環境不支援這個重新整理率的話,會自動降為30hz,而不是丟幀。而李蘭其在後臺的時候,聰明的瀏覽器會將這個渲染時機降為每秒4幀甚至更低,事件迴圈也會減少(這就是為什麼我們可以用setInterval來判斷時候能開啟其他app的判斷依據的原因)。如果能渲染的話會設定hasARenderingOpportunity為true。 >
除此之外,還會在觸發resize、scroll、建立媒體查詢、執行css動畫等,也就是說瀏覽器幾乎大部分使用者操作都發生在事件迴圈中,更具體點是事件迴圈中的ui render部分。之後會進行requestAnimationFrame和IntersectionObserver的觸發,再之後是ui渲染 12. 如果下面條件都成立,那麼執行空閒階段演算法,對於開發者來說就是呼叫window.requestIdleCallback方法 1. 在window環境下 1. event loop中沒有活躍的Task 1. 微任務佇列為空 1. hasARenderingOpportunity為false 借鑑網上的一張圖來粗略表示下整個流程 ![](https://user-gold-cdn.xitu.io/2020/6/24/172e47128372ad03?w=1036&h=1258&f=jpeg&s=120274) ## 小結 上面就是整個事件迴圈的流程,瀏覽器就是按照這個規則一遍遍的執行,而我們要做的就是了解並適應這個規則,讓瀏覽器渲染出效能更高的頁面。 比如: 1. 非首屏相關效能打點可以放到idle callback中執行,減少對頁面效能的損耗 1. 微任務中遞迴新增微任務會導致頁面卡死,而不是隨著事件迴圈一輪輪的執行 1. 更新元素佈局的最好時機是在requestAnimateFrame中 1. 儘量避免頻繁獲取元素佈局資訊,因為這會觸發強制layout([哪些屬性會導致強制layout?](https://gist.github.com/paulirish/5d52fb081b3570c81e3a)),影響頁面效能 1. 事件迴圈有多個任務佇列,他們互不衝突,但是使用者互動相關的優先順序更高 1. resize、scroll等會伴隨事件迴圈中ui渲染觸發,而不是根據我們的滾動觸發,換句話說,這些操作自帶節流 1. 等等,歡迎補充 最後感謝大家閱讀,歡迎一起探討! ## 提前祝大家端午節nb ## 參考 [composite](https://fed.taobao.org/blog/taofed/do71ct/performance-composite/?spm=taofed.blogs.blog-list.10.1eec5ac80f92Km) [深入探究 eventloop 與瀏覽器渲染的時序問題](https://juejin.im/entry/596d78ee6fb9a06bb7