成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

圖解 JavaScript 事件循環(huán):微任務(wù)和宏任務(wù)

開(kāi)發(fā) 前端
瀏覽器中 JavaScript 的執(zhí)行流程和 Node.js 中的流程都是基于 事件循環(huán) 的。

[[360759]]

事件循環(huán):微任務(wù)和宏任務(wù)

瀏覽器中 JavaScript 的執(zhí)行流程和 Node.js 中的流程都是基于 事件循環(huán) 的。

理解事件循環(huán)的工作方式對(duì)于代碼優(yōu)化很重要,有時(shí)對(duì)于正確的架構(gòu)也很重要。

在本章中,我們首先介紹有關(guān)事件循環(huán)工作方式的理論細(xì)節(jié),然后介紹該知識(shí)的實(shí)際應(yīng)用。

事件循環(huán)

事件循環(huán) 的概念非常簡(jiǎn)單。它是一個(gè)在 JavaScript 引擎等待任務(wù),執(zhí)行任務(wù)和進(jìn)入休眠狀態(tài)等待更多任務(wù)這幾個(gè)狀態(tài)之間轉(zhuǎn)換的無(wú)限循環(huán)。

引擎的一般算法:

1.當(dāng)有任務(wù)時(shí):

  • 從最先進(jìn)入的任務(wù)開(kāi)始執(zhí)行。

2.休眠直到出現(xiàn)任務(wù),然后轉(zhuǎn)到第 1 步。

當(dāng)我們?yōu)g覽一個(gè)網(wǎng)頁(yè)時(shí)就是上述這種形式。JavaScript 引擎大多數(shù)時(shí)候不執(zhí)行任何操作,它僅在腳本/處理程序/事件激活時(shí)執(zhí)行。

任務(wù)示例:

  • 當(dāng)外部腳本<script src="..."> 加載完成時(shí),任務(wù)就是執(zhí)行它。
  • 當(dāng)用戶移動(dòng)鼠標(biāo)時(shí),任務(wù)就是派生出 mousemove 事件和執(zhí)行處理程序。
  • 當(dāng)安排的(scheduled)setTimeout 時(shí)間到達(dá)時(shí),任務(wù)就是執(zhí)行其回調(diào)。
  • ……諸如此類。

設(shè)置任務(wù) —— 引擎處理它們 —— 然后等待更多任務(wù)(即休眠,幾乎不消耗 CPU 資源)。

一個(gè)任務(wù)到來(lái)時(shí),引擎可能正處于繁忙狀態(tài),那么這個(gè)任務(wù)就會(huì)被排入隊(duì)列。

多個(gè)任務(wù)組成了一個(gè)隊(duì)列,即所謂的“宏任務(wù)隊(duì)列”(v8 術(shù)語(yǔ)):

例如,當(dāng)引擎正在忙于執(zhí)行一段 script 時(shí),用戶可能會(huì)移動(dòng)鼠標(biāo)而產(chǎn)生 mousemove 事件,setTimeout 或許也剛好到期,以及其他任務(wù),這些任務(wù)組成了一個(gè)隊(duì)列,如上圖所示。

隊(duì)列中的任務(wù)基于“先進(jìn)先出”的原則執(zhí)行。當(dāng)瀏覽器引擎執(zhí)行完 script 后,它會(huì)處理 mousemove 事件,然后處理 setTimeout 處理程序,依此類推。

到目前為止,很簡(jiǎn)單,對(duì)吧?

兩個(gè)細(xì)節(jié):

  1. 引擎執(zhí)行任務(wù)時(shí)永遠(yuǎn)不會(huì)進(jìn)行渲染(render)。如果任務(wù)執(zhí)行需要很長(zhǎng)一段時(shí)間也沒(méi)關(guān)系。僅在任務(wù)完成后才會(huì)繪制對(duì) DOM 的更改。
  2. 如果一項(xiàng)任務(wù)執(zhí)行花費(fèi)的時(shí)間過(guò)長(zhǎng),瀏覽器將無(wú)法執(zhí)行其他任務(wù),例如處理用戶事件。因此,在一定時(shí)間后,瀏覽器會(huì)拋出一個(gè)如“頁(yè)面未響應(yīng)”之類的警報(bào),建議你終止這個(gè)任務(wù)。這種情況常發(fā)生在有大量復(fù)雜的計(jì)算或?qū)е滤姥h(huán)的程序錯(cuò)誤時(shí)。

以上是理論知識(shí)。現(xiàn)在,讓我們來(lái)看看如何應(yīng)用這些知識(shí)。

用例 1:拆分 CPU 過(guò)載任務(wù)

假設(shè)我們有一個(gè) CPU 過(guò)載任務(wù)。

例如,語(yǔ)法高亮(用來(lái)給本頁(yè)面中的示例代碼著色)是相當(dāng)耗費(fèi) CPU 資源的任務(wù)。為了高亮顯示代碼,它執(zhí)行分析,創(chuàng)建很多著了色的元素,然后將它們添加到文檔中 —— 對(duì)于文本量大的文檔來(lái)說(shuō),需要耗費(fèi)很長(zhǎng)時(shí)間。

當(dāng)引擎忙于語(yǔ)法高亮?xí)r,它就無(wú)法處理其他 DOM 相關(guān)的工作,例如處理用戶事件等。它甚至可能會(huì)導(dǎo)致瀏覽器“中斷(hiccup)”甚至“掛起(hang)”一段時(shí)間,這是不可接受的。

我們可以通過(guò)將大任務(wù)拆分成多個(gè)小任務(wù)來(lái)避免這個(gè)問(wèn)題。高亮顯示前 100 行,然后使用 setTimeout(延時(shí)參數(shù)為 0)來(lái)安排(schedule)后 100 行的高亮顯示,依此類推。

為了演示這種方法,簡(jiǎn)單起見(jiàn),讓我們寫一個(gè)從 1 數(shù)到 1000000000 的函數(shù),而不寫文本高亮。

如果你運(yùn)行下面這段代碼,你會(huì)看到引擎會(huì)“掛起”一段時(shí)間。對(duì)于服務(wù)端 JS 來(lái)說(shuō)這顯而易見(jiàn),并且如果你在瀏覽器中運(yùn)行它,嘗試點(diǎn)擊頁(yè)面上其他按鈕時(shí),你會(huì)發(fā)現(xiàn)在計(jì)數(shù)結(jié)束之前不會(huì)處理其他事件。

  1. let i = 0; 
  2.  
  3. let start = Date.now(); 
  4.  
  5. function count() { 
  6.  
  7.   // 做一個(gè)繁重的任務(wù) 
  8.   for (let j = 0; j < 1e9; j++) { 
  9.     i++; 
  10.   } 
  11.  
  12.   alert("Done in " + (Date.now() - start) + 'ms'); 
  13.  
  14. count(); 

瀏覽器甚至可能會(huì)顯示一個(gè)“腳本執(zhí)行時(shí)間過(guò)長(zhǎng)”的警告。

讓我們使用嵌套的 setTimeout 調(diào)用來(lái)拆分這個(gè)任務(wù):

  1. let i = 0; 
  2.  
  3. let start = Date.now(); 
  4.  
  5. function count() { 
  6.  
  7.   // 做繁重的任務(wù)的一部分 (*) 
  8.   do { 
  9.     i++; 
  10.   } while (i % 1e6 != 0); 
  11.  
  12.   if (i == 1e9) { 
  13.     alert("Done in " + (Date.now() - start) + 'ms'); 
  14.   } else { 
  15.     setTimeout(count); // 安排(schedule)新的調(diào)用 (**) 
  16.   } 
  17.  
  18.  
  19. count(); 

現(xiàn)在,瀏覽器界面在“計(jì)數(shù)”過(guò)程中可以正常使用。

單次執(zhí)行 count 會(huì)完成工作 (*) 的一部分,然后根據(jù)需要重新安排(schedule)自身的執(zhí)行 (**):

  1. 首先執(zhí)行計(jì)數(shù):i=1...1000000。
  2. 然后執(zhí)行計(jì)數(shù):i=1000001..2000000。
  3. ……以此類推。

現(xiàn)在,如果在引擎忙于執(zhí)行第一部分時(shí)出現(xiàn)了一個(gè)新的副任務(wù)(例如 onclick 事件),則該任務(wù)會(huì)被排入隊(duì)列,然后在第一部分執(zhí)行結(jié)束時(shí),并在下一部分開(kāi)始執(zhí)行前,會(huì)執(zhí)行該副任務(wù)。周期性地在兩次 count 執(zhí)行期間返回事件循環(huán),這為 JavaScript 引擎提供了足夠的“空氣”來(lái)執(zhí)行其他操作,以響應(yīng)其他的用戶行為。

值得注意的是這兩種變體 —— 是否使用了 setTimeout 對(duì)任務(wù)進(jìn)行拆分 —— 在執(zhí)行速度上是相當(dāng)?shù)摹T趫?zhí)行計(jì)數(shù)的總耗時(shí)上沒(méi)有多少差異。

為了使兩者耗時(shí)更接近,讓我們來(lái)做一個(gè)改進(jìn)。

我們將要把調(diào)度(scheduling)移動(dòng)到 count() 的開(kāi)頭:

  1. let i = 0; 
  2.  
  3. let start = Date.now(); 
  4.  
  5. function count() { 
  6.  
  7.   // 將調(diào)度(scheduling)移動(dòng)到開(kāi)頭 
  8.   if (i < 1e9 - 1e6) { 
  9.     setTimeout(count); // 安排(schedule)新的調(diào)用 
  10.   } 
  11.  
  12.   do { 
  13.     i++; 
  14.   } while (i % 1e6 != 0); 
  15.  
  16.   if (i == 1e9) { 
  17.     alert("Done in " + (Date.now() - start) + 'ms'); 
  18.   } 
  19.  
  20.  
  21. count(); 

現(xiàn)在,當(dāng)我們開(kāi)始調(diào)用 count() 時(shí),會(huì)看到我們需要對(duì) count() 進(jìn)行更多調(diào)用,我們就會(huì)在工作前立即安排(schedule)它。

如果你運(yùn)行它,你很容易注意到它花費(fèi)的時(shí)間明顯減少了。

為什么?

這很簡(jiǎn)單:你應(yīng)該還記得,多個(gè)嵌套的 setTimeout 調(diào)用在瀏覽器中的最小延遲為 4ms。即使我們?cè)O(shè)置了 0,但還是 4ms(或者更久一些)。所以我們安排(schedule)得越早,運(yùn)行速度也就越快。

最后,我們將一個(gè)繁重的任務(wù)拆分成了幾部分,現(xiàn)在它不會(huì)阻塞用戶界面了。而且其總耗時(shí)并不會(huì)長(zhǎng)很多。

用例 2:進(jìn)度指示

對(duì)瀏覽器腳本中的過(guò)載型任務(wù)進(jìn)行拆分的另一個(gè)好處是,我們可以顯示進(jìn)度指示。

正如前面所提到的,僅在當(dāng)前運(yùn)行的任務(wù)完成后,才會(huì)對(duì) DOM 中的更改進(jìn)行繪制,無(wú)論這個(gè)任務(wù)運(yùn)行花費(fèi)了多長(zhǎng)時(shí)間。

從一方面講,這非常好,因?yàn)槲覀兊暮瘮?shù)可能會(huì)創(chuàng)建很多元素,將它們一個(gè)接一個(gè)地插入到文檔中,并更改其樣式 —— 訪問(wèn)者不會(huì)看到任何未完成的“中間態(tài)”內(nèi)容。很重要,對(duì)吧?

這是一個(gè)示例,對(duì) i 的更改在該函數(shù)完成前不會(huì)顯示出來(lái),所以我們將只會(huì)看到最后的值:

  1. <div id="progress"></div> 
  2.  
  3. <script> 
  4.  
  5.   function count() { 
  6.     for (let i = 0; i < 1e6; i++) { 
  7.       i++; 
  8.       progress.innerHTML = i; 
  9.     } 
  10.   } 
  11.  
  12.   count(); 
  13. </script> 

 

 

 

 

……但是我們也可能想在任務(wù)執(zhí)行期間展示一些東西,例如進(jìn)度條。

如果我們使用 setTimeout 將繁重的任務(wù)拆分成幾部分,那么變化就會(huì)被在它們之間繪制出來(lái)。

這看起來(lái)更好看:

  1. <div id="progress"></div> 
  2.  
  3. <script> 
  4.   let i = 0; 
  5.  
  6.   function count() { 
  7.  
  8.     // 做繁重的任務(wù)的一部分 (*) 
  9.     do { 
  10.       i++; 
  11.       progress.innerHTML = i; 
  12.     } while (i % 1e3 != 0); 
  13.  
  14.     if (i < 1e7) { 
  15.       setTimeout(count); 
  16.     } 
  17.  
  18.   } 
  19.  
  20.   count(); 
  21. </script> 

 

 

 

 

 

現(xiàn)在 div 顯示了 i 的值的增長(zhǎng),這就是進(jìn)度條的一種。

用例 3:在事件之后做一些事情

在事件處理程序中,我們可能會(huì)決定推遲某些行為,直到事件冒泡并在所有級(jí)別上得到處理后。我們可以通過(guò)將該代碼包裝到零延遲的 setTimeout 中來(lái)做到這一點(diǎn)。

在 創(chuàng)建自定義事件[1] 一章中,我們看到過(guò)這樣一個(gè)例子:自定義事件 menu-open 被在 setTimeout 中分派(dispatched),所以它在 click 事件被處理完成之后發(fā)生。

  1. menu.onclick = function() { 
  2.   // ... 
  3.  
  4.   // 創(chuàng)建一個(gè)具有被點(diǎn)擊的菜單項(xiàng)的數(shù)據(jù)的自定義事件 
  5.   let customEvent = new CustomEvent("menu-open", { 
  6.     bubbles: true 
  7.   }); 
  8.  
  9.   // 異步分派(dispatch)自定義事件 
  10.   setTimeout(() => menu.dispatchEvent(customEvent)); 
  11. }; 

宏任務(wù)和微任務(wù)

除了本章中所講的 宏任務(wù)(macrotask) 外,還有在 微任務(wù)隊(duì)列[2] 一章中提到的 微任務(wù)(microtask)。

微任務(wù)僅來(lái)自于我們的代碼。它們通常是由 promise 創(chuàng)建的:對(duì) .then/catch/finally 處理程序的執(zhí)行會(huì)成為微任務(wù)。微任務(wù)也被用于 await 的“幕后”,因?yàn)樗?promise 處理的另一種形式。

還有一個(gè)特殊的函數(shù) queueMicrotask(func),它對(duì) func 進(jìn)行排隊(duì),以在微任務(wù)隊(duì)列中執(zhí)行。

每個(gè)宏任務(wù)之后,引擎會(huì)立即執(zhí)行微任務(wù)隊(duì)列中的所有任務(wù),然后再執(zhí)行其他的宏任務(wù),或渲染,或進(jìn)行其他任何操作。

例如,看看下面這個(gè)示例:

  1. setTimeout(() => alert("timeout")); 
  2.  
  3. Promise.resolve() 
  4.   .then(() => alert("promise")); 
  5.  
  6. alert("code"); 

這里的執(zhí)行順序是怎樣的?

  1. code 首先顯示,因?yàn)樗浅R?guī)的同步調(diào)用。
  2. promise 第二個(gè)出現(xiàn),因?yàn)?then 會(huì)通過(guò)微任務(wù)隊(duì)列,并在當(dāng)前代碼之后執(zhí)行。
  3. timeout 最后顯示,因?yàn)樗且粋€(gè)宏任務(wù)。

更詳細(xì)的事件循環(huán)圖示如下(順序是從上到下,即:首先是腳本,然后是微任務(wù),渲染等):

 

微任務(wù)會(huì)在執(zhí)行任何其他事件處理,或渲染,或執(zhí)行任何其他宏任務(wù)之前完成。

這很重要,因?yàn)樗_保了微任務(wù)之間的應(yīng)用程序環(huán)境基本相同(沒(méi)有鼠標(biāo)坐標(biāo)更改,沒(méi)有新的網(wǎng)絡(luò)數(shù)據(jù)等)。

如果我們想要異步執(zhí)行(在當(dāng)前代碼之后)一個(gè)函數(shù),但是要在更改被渲染或新事件被處理之前執(zhí)行,那么我們可以使用 queueMicrotask 來(lái)對(duì)其進(jìn)行安排(schedule)。

這是一個(gè)與前面那個(gè)例子類似的,帶有“計(jì)數(shù)進(jìn)度條”的示例,但是它使用了 queueMicrotask而不是 setTimeout。你可以看到它在最后才渲染。就像寫的是同步代碼一樣:

  1. <div id="progress"></div> 
  2.  
  3. <script> 
  4.   let i = 0; 
  5.  
  6.   function count() { 
  7.  
  8.     // 做繁重的任務(wù)的一部分 (*) 
  9.     do { 
  10.       i++; 
  11.       progress.innerHTML = i; 
  12.     } while (i % 1e3 != 0); 
  13.  
  14.     if (i < 1e6) { 
  15.       queueMicrotask(count); 
  16.     } 
  17.  
  18.   } 
  19.  
  20.   count(); 
  21. </script> 

 

 

 

總結(jié)

更詳細(xì)的事件循環(huán)算法(盡管與 規(guī)范[3] 相比仍然是簡(jiǎn)化過(guò)的):

1.從 宏任務(wù) 隊(duì)列(例如 "script")中出隊(duì)(dequeue)并執(zhí)行最早的任務(wù)。

2.執(zhí)行所有 微任務(wù):

  • 出隊(duì)(dequeue)并執(zhí)行最早的微任務(wù)。
  • 當(dāng)微任務(wù)隊(duì)列非空時(shí):

3.執(zhí)行渲染,如果有。

4.如果宏任務(wù)隊(duì)列為空,則休眠直到出現(xiàn)宏任務(wù)。

5.轉(zhuǎn)到步驟 1。

安排(schedule)一個(gè)新的 宏任務(wù):

  • 使用零延遲的 setTimeout(f)。

它可被用于將繁重的計(jì)算任務(wù)拆分成多個(gè)部分,以使瀏覽器能夠?qū)τ脩羰录鞒龇磻?yīng),并在任務(wù)的各部分之間顯示任務(wù)進(jìn)度。

此外,也被用于在事件處理程序中,將一個(gè)行為(action)安排(schedule)在事件被完全處理(冒泡完成)后。

安排一個(gè)新的 微任務(wù):

  • 使用 queueMicrotask(f)。
  • promise 處理程序也會(huì)通過(guò)微任務(wù)隊(duì)列。

在微任務(wù)之間沒(méi)有 UI 或網(wǎng)絡(luò)事件的處理:它們一個(gè)立即接一個(gè)地執(zhí)行。

所以,我們可以使用 queueMicrotask 來(lái)在保持環(huán)境狀態(tài)一致的情況下,異步地執(zhí)行一個(gè)函數(shù)。

Web Workers:

對(duì)于不應(yīng)該阻塞事件循環(huán)的耗時(shí)長(zhǎng)的繁重計(jì)算任務(wù),我們可以使用 Web Workers[4]。

這是在另一個(gè)并行線程中運(yùn)行代碼的方式。

Web Workers 可以與主線程交換消息,但是它們具有自己的變量和事件循環(huán)。

Web Workers 沒(méi)有訪問(wèn) DOM 的權(quán)限,因此,它們對(duì)于同時(shí)使用多個(gè) CPU 內(nèi)核的計(jì)算非常有用。

參考資料

 

[1]創(chuàng)建自定義事件: https://zh.javascript.info/dispatch-events

[2]微任務(wù)隊(duì)列: https://zh.javascript.info/microtask-queue

[3]規(guī)范: https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model

[4]Web Workers: https://html.spec.whatwg.org/multipage/workers.html

[5]React 官方文檔推薦,與 MDN 并列的 JavaScript 學(xué)習(xí)教程: https://zh-hans.reactjs.org/docs/getting-started.html#javascript-resources

本文轉(zhuǎn)載自微信公眾號(hào)「技術(shù)漫談」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系技術(shù)漫談公眾號(hào)。

 

責(zé)任編輯:武曉燕 來(lái)源: 技術(shù)漫談
相關(guān)推薦

2022-06-13 10:24:47

宏任務(wù)微任務(wù)前端

2021-01-18 08:24:51

JavaScriptMicrotask微任務(wù)

2023-04-06 00:22:19

JavaScrip任務(wù)開(kāi)發(fā)

2021-07-24 11:15:19

開(kāi)發(fā)技能代碼

2021-08-03 07:40:47

宏任務(wù)微任務(wù)React

2017-05-02 22:38:44

前端開(kāi)發(fā)JS事件循環(huán)機(jī)制

2023-11-13 07:37:36

JS面試題線程

2021-08-17 09:55:05

JavaScript MicrotaskPromise

2024-10-23 16:02:40

JavaScriptPromiserejection

2011-06-16 16:20:32

JavaScript分解任務(wù)

2016-09-06 21:23:25

JavaScriptnode異步

2009-03-17 15:36:29

JavaScript循環(huán)事件

2019-12-20 14:19:50

Windows 10操作系統(tǒng)

2022-06-20 07:44:34

ahooks定時(shí)器

2021-12-04 22:05:41

網(wǎng)頁(yè)任務(wù) Performanc

2019-11-14 10:00:18

Linuxcron任務(wù)自動(dòng)化任務(wù)

2021-12-25 22:29:57

Node.js 微任務(wù)處理事件循環(huán)

2020-04-26 08:21:43

javascriptVue

2022-08-18 11:36:16

可視化JavaScript事件循環(huán)

2021-04-26 07:53:04

繪制流程任務(wù)
點(diǎn)贊
收藏

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

主站蜘蛛池模板: www.操.com | 国产亚洲精品久久久久动 | 久久性色| 久久久久久久久国产 | 精品国产一区二区三区观看不卡 | 国产内谢| 伊人激情综合网 | 精品国产乱码久久久久久中文 | 久久精品手机视频 | 中文字幕亚洲一区 | 欧美日韩一区二区视频在线观看 | 欧美久久久久久 | 日本黄色免费大片 | 91亚洲视频在线 | 日日干天天干 | 天天操夜夜操 | 一区二区三区在线电影 | 伊人国产精品 | 亚洲成在线观看 | 久久高清精品 | 欧美日韩精品在线免费观看 | 在线观看免费av网 | 成人国产在线视频 | 中文亚洲视频 | 国产日韩一区二区三免费 | 午夜视频网站 | 女同久久另类99精品国产 | 欧美高清视频在线观看 | 亚洲国产精品久久久久久 | 成人超碰| 九九在线 | 免费观看的av毛片的网站 | 在线 丝袜 欧美 日韩 制服 | 亚洲视频免费在线观看 | 精品日韩一区 | 暖暖成人免费视频 | 午夜免费福利片 | 秋霞性生活| 伊大人久久 | 欧美日韩黄色一级片 | 日日夜夜天天干 |