偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

JavaScript 異步編程指南 - 探索瀏覽器中的事件循環(huán)機制

開發(fā) 前端
當(dāng)我了解事件循環(huán)時,嘗試去找一些規(guī)范來學(xué)習(xí),但是查遍 EcmaScript 或 V8 發(fā)現(xiàn)它們沒有這個東西的定義,例如,在 v8 里有的是執(zhí)行棧、堆這些信息。確實,事件循環(huán)不在這里。

[[429067]]

當(dāng)我了解事件循環(huán)時,嘗試去找一些規(guī)范來學(xué)習(xí),但是查遍 EcmaScript 或 V8 發(fā)現(xiàn)它們沒有這個東西的定義,例如,在 v8 里有的是執(zhí)行棧、堆這些信息。確實,事件循環(huán)不在這里。

后來才逐漸的了解到,當(dāng)在瀏覽器環(huán)境中,關(guān)于事件循環(huán)相關(guān)定義是在 HTML 標(biāo)準(zhǔn)中,之前 HTML 規(guī)范由 whatwg 和 w3c 制定,兩個組織都有自己的不同,2019 年時兩個組織簽署了一項協(xié)議 就 HTML 和 DOM 的單一版本進行合作,最終,HTML、DOM 標(biāo)準(zhǔn)最終由 whatwg 維護。

本文的講解主要也是以 whatwg 標(biāo)準(zhǔn)為主,在 HTML Living Standard Event loops 中,這個規(guī)范定義了瀏覽器內(nèi)核該如何的去實現(xiàn)它。

瀏覽器規(guī)范中的事件循環(huán)

事件循環(huán)定義

為了協(xié)調(diào)事件、用戶交互、腳本、渲染、網(wǎng)絡(luò)等,用戶代理必須使用本節(jié)描述的事件循環(huán)。每個代理有一個關(guān)聯(lián)的事件循環(huán),它對每個代理是唯一的。

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. Each agent has an associated event loop, which is unique to that agent.

從這個定義也可看出,事件循環(huán)主要是用來協(xié)調(diào)事件、網(wǎng)絡(luò)、JavaScript 等之間的一個運行機制,我們以 JavaScript 為出發(fā)點來看下它們之間是如何交互的。

事件循環(huán)中有一個重要的概念任務(wù)隊列,它決定了任務(wù)的執(zhí)行順序。

事件循環(huán)的處理模式

規(guī)范 8.1.6.3 處理模型 定義了事件循環(huán)的處理模式,當(dāng)一個事件循環(huán)存在,它就會不斷的執(zhí)行以下步驟:

這些概念很晦澀難懂,簡單總結(jié)下:

  • 執(zhí)行 Task:任務(wù)隊列有多個任務(wù)源(DOM、UI、網(wǎng)絡(luò)等)隊列,從中至少選出一個可運行的任務(wù),放到 taskQueue 中。
    • 如果沒有直接跳到微任務(wù)隊列,就不會經(jīng)過 2 ~ 5。
    • 否則從 taskQueue 中取出第一個可執(zhí)行任務(wù)做為 oldestTask 執(zhí)行,對應(yīng) 2 ~ 5。
    • 注意,微任務(wù)不會在這里被選中,但是當(dāng)一個任務(wù)隊列里含有微任務(wù),會將該微任務(wù)加入微任務(wù)隊列。
    • 執(zhí)行 Microtask:執(zhí)行微任務(wù)隊列,直到微任務(wù)隊列為空,這里如果調(diào)度太多的微任務(wù)也會導(dǎo)致阻塞。
    • 更新渲染。

看到一個圖,描述一次事件循環(huán)的過程,差不多就是這個意思,主要呢,還是這三個階段:Task、Microtask、Render 下文會展開的討論。

圖片來源:https://pic2.zhimg.com/80/v2-38e53b9df2d13e9470c31101bb82dbb1_1440w.jpg

Task(Macrotask)

之前也看過很多文章關(guān)于事件循環(huán)的介紹,**大多會把 “Task” 當(dāng)作 “Marcotask” 也就是宏任務(wù)來介紹,但是在規(guī)范中沒有所謂的 “Marcotask”,**因為規(guī)范里沒有這個名詞,所以我在這個標(biāo)題上特意加了個括號,有很多的叫法,也有稱為外部隊列的,這其實是一個意思,如果你是學(xué)習(xí)事件循環(huán)的新朋友可能就會有疑問,為什么我搜索不到關(guān)于這個的解釋。

下文我會繼續(xù)使用規(guī)范中的名詞 “任務(wù)隊列” 來表達。

任務(wù)隊列是一個任務(wù)的集合。事件循環(huán)有一個或多個任務(wù)隊列,事件循環(huán)做的第一步是從選擇的隊列中獲取第一個可運行的任務(wù),而不是出列第一個任務(wù)。

傳統(tǒng)的隊列(Queue)是一個先進先出的數(shù)據(jù)結(jié)構(gòu),總是排在第一個的先執(zhí)行,而這里的隊列里面會包含一些類似于 setTimeout 這樣延遲執(zhí)行的任務(wù),所以,在規(guī)范中有這樣一句話:“Task queues are sets, not queues(翻譯為任務(wù)隊列是一個集合,不是隊列)”。

任務(wù)隊列的 任務(wù)源 主要包括以下這些:

  • DOM 操作:對 DOM 操作產(chǎn)生的任務(wù),例如,將元素插入文檔時以非阻塞方式發(fā)生的事情 document.body = aNewBodyElement;。
  • 用戶交互:用戶交互產(chǎn)生的任務(wù),例如鼠標(biāo)點擊、移動產(chǎn)生的 Callback 任務(wù)。
  • 網(wǎng)絡(luò):網(wǎng)絡(luò)請求產(chǎn)生的任務(wù),例如 fetch()。
  • 歷史遍歷:此任務(wù)源用于對 history.back() 和類似 API 的調(diào)用進行排隊。
  • **setTimeout、setInterval:**定時器相關(guān)任務(wù)。

例如,當(dāng) User agent 有一個管理鼠標(biāo)和鍵盤事件的任務(wù)隊列和另一個其它任務(wù)源相關(guān)的任務(wù)隊列,在事件循環(huán)中相比其它任務(wù),它會多出四分之三的時間來優(yōu)先執(zhí)行鼠標(biāo)和鍵盤事件的任務(wù)隊列,這樣使得其它任務(wù)源的任務(wù)隊列在能夠得到處理的情況下用戶交互相關(guān)的任務(wù)可以得到更高優(yōu)先級的處理,這也是提高了用戶的體驗。

Microtask

每個事件循環(huán)有一個微任務(wù)隊列,它不是一個 task queue,兩者是獨立的隊列。

什么是 Microtask(微任務(wù))

微任務(wù)是一個簡短的函數(shù),當(dāng)創(chuàng)建該函數(shù)的函數(shù)執(zhí)行后,并且 JavaScript 執(zhí)行上下文棧為空,而控制權(quán)尚未交還給事件循環(huán)之前觸發(fā)。

當(dāng)我們在一個微任務(wù)里通過 queueMicrotask(callback) 繼續(xù)向微任務(wù)隊列中創(chuàng)建更多的任務(wù),對于事件循環(huán)來說,它仍會持續(xù)調(diào)用微任務(wù)直至隊列為空。

  1. const log = console.log; 
  2. let i = 0; 
  3. log('sync run start'); 
  4. runMicrotask(); 
  5. log('sync run end'); 
  6.  
  7. function runMicrotask() { 
  8.   queueMicrotask(() => { 
  9.     log("microtask run, i = ", i++); 
  10.     if (i > 10) return;  
  11.     runMicrotask(); 
  12.   }); 

上面這段代碼很簡單,在主線程調(diào)用了 runMicrotask() 函數(shù),該函數(shù)內(nèi)部使用 queueMicrotask() 創(chuàng)建了微任務(wù)并且遞歸調(diào)用,微任務(wù)的觸發(fā)是在執(zhí)行棧為空時才執(zhí)行,因為里面遞歸調(diào)用每次都會生成新的微任務(wù),事件循環(huán)也是在微任務(wù)執(zhí)行完畢才執(zhí)行 Task Queue 里面的 setTimeout 回調(diào)。

  1. sync run start 
  2. sync run end 
  3. microtask run, i = 0 
  4. microtask run, i = 1 
  5. microtask run, i = 2 
  6. microtask run, i = 3 
  7. microtask run, i = 4 
  8. microtask run, i = 5 
  9. microtask run, i = 6 
  10. microtask run, i = 7 
  11. microtask run, i = 8 
  12. microtask run, i = 9 
  13. microtask run, i = 10 

通過這個示例,也可看到當(dāng)調(diào)度大量的微任務(wù)也會導(dǎo)致和同步任務(wù)相同的性能缺陷,后面的任務(wù)得不到執(zhí)行,瀏覽器的渲染工作也會被阻止。微任務(wù)這里的隊列才是真正的隊列。

創(chuàng)建一個 Microtask(Promise VS queueMicrotask)

在以往我們創(chuàng)建一個微任務(wù)很簡單,可以創(chuàng)建一個立即 resolve 的 Promise,每次都需要創(chuàng)建一個 Promise 實例,同時也帶來了額外的內(nèi)存開銷,另外 Promise 中拋出的錯誤是一個非標(biāo)準(zhǔn)的 Error,如果未正常捕獲通常會得到這樣一個錯誤 UnhandledPromiseRejectionWarning:。

使用 Promise 創(chuàng)建一個微任務(wù)。

  1. const p = new Promise((resolve, reject) => { 
  2.   // reject('err'
  3.   resolve(1); 
  4. }); 
  5. p.then(() => { 
  6.   log('Promise microtask.'
  7. }); 

現(xiàn)在 Window 對象上提供了 queueMicrotask() 方法以一種標(biāo)準(zhǔn)的方式,可以安全的引入微任務(wù),而無需使用額外的技巧,它提供了一種標(biāo)準(zhǔn)的異常。

使用 queueMicrotask() 創(chuàng)建一個微任務(wù)。

  1. queueMicrotask(() => { 
  2.   log('queueMicrotask.'); 
  3. }); 

在我們寫業(yè)務(wù)功能時,一個功能或方法內(nèi)涉及多個異步調(diào)度的任務(wù)也是很常見的,基于 Promise 我們很熟悉,還可以使用 Async/Await 以一種同步線性的思維來書寫代碼。而 queueMicrotask 需要傳遞一個回調(diào)函數(shù),當(dāng)層級多了很容易出現(xiàn)嵌套。

重點是大多數(shù)情況下我們也不需要去創(chuàng)建微任務(wù),過多的濫用也會造成性能問題,也許在做一些類似創(chuàng)建框架或庫時可能需要借助微任務(wù)來達到某些功能。這里我想到了一個經(jīng)常問的面試題 “實現(xiàn)一個 Promise” 這個在實現(xiàn)時也許可以采用 queueMicrotask(),在《JavaScript 異步編程》的源碼系列,會再看到這個問題。

Microtask 總結(jié)

Microtask 總結(jié)一句話來講就是:“它是在當(dāng)前執(zhí)行棧尾部下一次事件循環(huán)前執(zhí)行”,需要注意的是,事件循環(huán)在處理微任務(wù)時,如果微任務(wù)隊列不為空,就會繼續(xù)執(zhí)行微任務(wù),例如,使用遞歸不停的增加新的微任務(wù),這就很糟糕了。

微任務(wù)所包含的任務(wù)源沒有明確的定義,通常包括這幾個:Promise.then()、Object.observe(已廢棄)、MutaionObserver、queueMicrotask。

更新渲染

渲染是事件循環(huán)中另一個很重要的階段,這里有一個關(guān)于 瀏覽器工作原理 的講解很好,整個渲染過程,理解下來主要是下面幾個步驟,其中 Layout、 **Paint **這些詞在下面的示例還會再次看到。

  • 解析 HTML 文檔轉(zhuǎn)化為 DOM Tree,同時也會解析外部 CSS 文件及內(nèi)嵌的 CSS 樣式為 CSSOM Tree。
  • DOM Tree、CSSOM Tree 兩者的結(jié)合創(chuàng)建出另外一個樹結(jié)構(gòu) Render Tree。
  • Render Tree 完畢之后進入布局(Layout)階段,為每個節(jié)點分配一個在屏幕上的坐標(biāo)位置。
  • 接下來根據(jù)節(jié)點坐標(biāo)位置對整個頁面繪制(Paint)。
  • 當(dāng)我們對 DOM 元素修改之后,例如元素顏色改變、添加 DOM 節(jié)點,這時也還會觸發(fā)布局和重繪(Repaint)。

圖片來源:https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/webkitflow.png

結(jié)合 Task 與 Microtask 看渲染過程

做一個測試,使用 queueMicrotask 創(chuàng)建一個微任務(wù),在自定義的 runMicrotask() 函數(shù)內(nèi)部遞歸調(diào)用了 10 次,每一次里我都希望來回變換 container 這個 div 的背景色,另外還放置了一個 setTimeout 屬于 Task queue 這個是讓大家順便看下 Task queue 在事件循環(huán)中的執(zhí)行順序

  1. <div id="container" style="width: 200px; height: 200px; background-color: red; font-size: 100px; color: #fff;"
  2.   0 
  3. </div> 
  4. <script> 
  5.   let i = 0; 
  6.   const container = document.getElementById('container'); 
  7.   setTimeout(() => {}); 
  8.   runMicrotask(); 
  9.   function runMicrotask() { 
  10.     queueMicrotask(() => { 
  11.       if (i > 10) return;  
  12.       container.innerText = i; 
  13.       container.style.backgroundColor = i % 2 === 0 ? 'blue' : 'red'
  14.       runMicrotask(); 
  15.     }); 
  16.   } 
  17. </script> 

通過 Chrome 的 Performance 記錄,運行過程,首先看下 Frame 只有一個,直接渲染出了最后的結(jié)果,如果按照上例,我們可能會覺得應(yīng)該是在每個微任務(wù)執(zhí)行時都會有一次渲染 blue -> red -> blue -> ...

再看一個更詳細的執(zhí)行過程,可以看到在執(zhí)行腳步執(zhí)行后,首先運行的是微任務(wù),對應(yīng)的是我們代碼 runMicrotask() 函數(shù),下圖紫色的是 Layout,Paint 是渲染繪制能夠看到就是在運行完所有的微任務(wù)之后執(zhí)行的,在之后是下一次事件循環(huán)最后執(zhí)行了 Task Queue Timer。

根據(jù)事件循環(huán)處理模式規(guī)范中的描述,渲染是在一次事件循環(huán)的微任務(wù)結(jié)束之后運行,上例差不多驗證了這個結(jié)果,這個時候有個疑問:“為什么不是在每一次微任務(wù)結(jié)束之后執(zhí)行,當(dāng)你把 queueMicrotask 替換成 setTimeout 也是一樣的,不會在每次事件中都去執(zhí)行”。

Render 在事件循環(huán)中什么時候執(zhí)行?

規(guī)范中還有這樣一段描述,得到一個信息是:在每一次的事件循環(huán)結(jié)束后不一定會執(zhí)行渲染。

每一輪的事件循環(huán)如果沒有阻塞操作,這個時間是很快的,考慮到硬件刷新頻率限制和性能原因的 user agent 節(jié)流,瀏覽器的更新渲染不會在每次事件循環(huán)中被觸發(fā)。如果瀏覽器試圖達到每秒 60Hz 的刷新率,也簡稱 60fps(60 frame per second),這時繪制一個 Frame 的間隔為 16.67ms(1000/60)。如果在 16ms 內(nèi)有多次 DOM 操作,也是不會渲染多次的。

如果瀏覽器無法維持 60fps 就會降低到 30fps、4fps 甚至更低。

如果想在每次事件循環(huán)中或微任務(wù)之后執(zhí)行一次繪制,可以通過 requestAnimationFrame 重新渲染。

結(jié)合 requestAnimationFrame 再看渲染過程

requestAnimationFrame 是瀏覽器 window 對象下提供的一個 API,它的應(yīng)用場景是告訴瀏覽器,我需要運行一個動畫。該方法會要求瀏覽器在下次重繪之前調(diào)用指定的回調(diào)函數(shù)更新動畫。

修改上述示例,加上 requestAnimationFrame() 方法。

  1. function runMicrotask() { 
  2.     queueMicrotask(() => { 
  3.       requestAnimationFrame(() => { 
  4.         if (i > 10) return;  
  5.         container.innerText = i; 
  6.         container.style.backgroundColor = i % 2 === 0 ? 'blue' : 'red'
  7.         i++; 
  8.         runMicrotask(); 
  9.       }); 
  10.     }); 
  11.   } 

運行之后如下所示,每一次的元素改變都得到了重新繪制。

放大其中一個看看任務(wù)的執(zhí)行情況,requestAnimationFrame 也可以看作一個任務(wù),可以看到它在運行之后執(zhí)行微任務(wù)。

Render 總結(jié)

事件循環(huán)中 Render 階段可能在一次事件循環(huán)中運行,也可能在多次事件循環(huán)后運行。它會受到瀏覽器的刷新頻率影響,如果是 60fps 那就是每間隔 16.67ms 執(zhí)行一次,另一方面當(dāng)瀏覽器認為更新渲染對用戶沒有影響的情況下,也會認為這不是一次必要的渲染。

總的來說它的機制和瀏覽器是相關(guān)的,了解即可,不用特別的糾結(jié)。

總結(jié)

瀏覽器中事件循環(huán)主要由 Task、Microtask、Render 三個階段組成,Task、Microtask 是我們會用到的比較多的,無論是網(wǎng)絡(luò)請求、還是 DOM 操作、Promise 這些大致都劃分為這兩類任務(wù),每一輪的事件循環(huán)都會檢查這兩個任務(wù)隊列里是否有要執(zhí)行的任務(wù),等 JavaScript 上下文棧空后,先情況微任務(wù)隊列里的所有任務(wù),之后在執(zhí)行宏任務(wù),而 Render 則不是必須的,它受瀏覽器的一些因素影響,并不一定在每次事件循環(huán)中執(zhí)行。

Reference

https://yu-jack.github.io/2020/02/03/javascript-runtime-event-loop-browser/

https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/

https://html.spec.whatwg.org/multipage/webappapis.html#event-loops

 

https://zhuanlan.zhihu.com/p/34229323

 

責(zé)任編輯:武曉燕 來源: 編程界
相關(guān)推薦

2021-10-22 08:29:14

JavaScript事件循環(huán)

2017-01-05 09:07:25

JavaScript瀏覽器驅(qū)動

2016-10-09 08:38:01

JavaScript瀏覽器事件

2015-04-22 10:50:18

JavascriptJavascript異

2014-05-23 10:12:20

Javascript異步編程

2021-12-08 07:55:41

EventLoop瀏覽器事件

2017-02-09 15:15:54

Chrome瀏覽器

2020-12-23 07:37:17

瀏覽器HTML DOM0

2019-12-17 14:45:17

瀏覽器事件循環(huán)前端

2023-04-28 15:20:37

JavaScript事件循環(huán)

2024-06-04 15:56:48

Task?.NET異步編程

2013-03-08 09:33:25

JavaScript同步異步

2021-06-10 07:51:07

Node.js循環(huán)機制

2017-04-26 14:15:35

瀏覽器緩存機制

2021-06-06 19:51:07

JavaScript異步編程

2017-05-15 13:40:20

瀏覽器http緩存機制

2022-07-07 07:22:01

瀏覽器JavaScript工具

2013-04-01 15:25:41

異步編程異步EMP

2020-09-28 14:41:24

Event Loop

2020-03-12 11:29:51

JavaScript瀏覽器語言
點贊
收藏

51CTO技術(shù)棧公眾號