自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

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

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

[[429067]]

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

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

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

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

事件循環(huán)定義

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

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.

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

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

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

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

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

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

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

圖片來(lái)源:https://pic2.zhimg.com/80/v2-38e53b9df2d13e9470c31101bb82dbb1_1440w.jpg

Task(Macrotask)

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

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

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

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

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

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

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

Microtask

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

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

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

當(dāng)我們?cè)谝粋€(gè)微任務(wù)里通過(guò) queueMicrotask(callback) 繼續(xù)向微任務(wù)隊(duì)列中創(chuàng)建更多的任務(wù),對(duì)于事件循環(huán)來(lái)說(shuō),它仍會(huì)持續(xù)調(diào)用微任務(wù)直至隊(duì)列為空。

  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.   }); 

上面這段代碼很簡(jiǎn)單,在主線程調(diào)用了 runMicrotask() 函數(shù),該函數(shù)內(nèi)部使用 queueMicrotask() 創(chuàng)建了微任務(wù)并且遞歸調(diào)用,微任務(wù)的觸發(fā)是在執(zhí)行棧為空時(shí)才執(zhí)行,因?yàn)槔锩孢f歸調(diào)用每次都會(huì)生成新的微任務(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 

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

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

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

使用 Promise 創(chuàng)建一個(gè)微任務(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 對(duì)象上提供了 queueMicrotask() 方法以一種標(biāo)準(zhǔn)的方式,可以安全的引入微任務(wù),而無(wú)需使用額外的技巧,它提供了一種標(biāo)準(zhǔn)的異常。

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

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

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

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

Microtask 總結(jié)

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

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

更新渲染

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

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

圖片來(lái)源:https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/webkitflow.png

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

做一個(gè)測(cè)試,使用 queueMicrotask 創(chuàng)建一個(gè)微任務(wù),在自定義的 runMicrotask() 函數(shù)內(nèi)部遞歸調(diào)用了 10 次,每一次里我都希望來(lái)回變換 container 這個(gè) div 的背景色,另外還放置了一個(gè) setTimeout 屬于 Task queue 這個(gè)是讓大家順便看下 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> 

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

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

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

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

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

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

如果瀏覽器無(wú)法維持 60fps 就會(huì)降低到 30fps、4fps 甚至更低。

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

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

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

修改上述示例,加上 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.   } 

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

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

Render 總結(jié)

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

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

總結(jié)

瀏覽器中事件循環(huán)主要由 Task、Microtask、Render 三個(gè)階段組成,Task、Microtask 是我們會(huì)用到的比較多的,無(wú)論是網(wǎng)絡(luò)請(qǐng)求、還是 DOM 操作、Promise 這些大致都劃分為這兩類任務(wù),每一輪的事件循環(huán)都會(huì)檢查這兩個(gè)任務(wù)隊(duì)列里是否有要執(zhí)行的任務(wù),等 JavaScript 上下文??蘸螅惹闆r微任務(wù)隊(duì)列里的所有任務(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é)任編輯:武曉燕 來(lái)源: 編程界
相關(guān)推薦

2021-10-22 08:29:14

JavaScript事件循環(huán)

2017-01-05 09:07:25

JavaScript瀏覽器驅(qū)動(dòng)

2014-05-23 10:12:20

Javascript異步編程

2015-04-22 10:50:18

JavascriptJavascript異

2016-10-09 08:38:01

JavaScript瀏覽器事件

2021-12-08 07:55:41

EventLoop瀏覽器事件

2017-02-09 15:15:54

Chrome瀏覽器

2019-12-17 14:45:17

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

2020-12-23 07:37:17

瀏覽器HTML DOM0

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)機(jī)制

2021-06-06 19:51:07

JavaScript異步編程

2017-04-26 14:15:35

瀏覽器緩存機(jī)制

2017-05-15 13:40:20

瀏覽器http緩存機(jī)制

2022-07-07 07:22:01

瀏覽器JavaScript工具

2020-03-12 11:29:51

JavaScript瀏覽器語(yǔ)言

2013-04-01 15:25:41

異步編程異步EMP

2020-11-13 11:15:17

數(shù)據(jù)加密攻擊模型瀏覽器密碼
點(diǎn)贊
收藏

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