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

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

了解事件循環(huán)的工作方式對于優(yōu)化(有時對于正確的體系結(jié)構(gòu))非常重要。

在本章中,我們首先介紹有關(guān)事物如何工作的理論細(xì)節(jié),然后介紹該知識的實際應(yīng)用。

事件循環(huán)

事件循環(huán)的概念是很簡單的。有一個無限循環(huán),JavaScript引擎等待任務(wù),執(zhí)行任務(wù),然后休眠,等待更多任務(wù)。

引擎的一般算法:

  1. 當(dāng)有任務(wù):
    • 從最早的任務(wù)開始執(zhí)行它們。
  2. 休眠直到出現(xiàn)任務(wù),然后轉(zhuǎn)到步驟1。

這是瀏覽頁面時看到的形式化信息。JavaScript引擎大部分時間不執(zhí)行任何操作,僅在腳本/處理程序/事件被激活時才運行。

任務(wù)示例:

  • <script src="...">加載外部腳本時,任務(wù)是執(zhí)行它。
  • 用戶移動鼠標(biāo)時,任務(wù)是調(diào)度mousemove事件并執(zhí)行處理程序。
  • 當(dāng)計劃好的時間到了時setTimeout,任務(wù)是運行其回調(diào)。
  • 等等。

設(shè)置任務(wù)-引擎處理它們-然后等待更多任務(wù)(在睡眠時消耗接近零的CPU)。

可能是在引擎繁忙時任務(wù)來了,然后才入隊了。

這些任務(wù)形成一個隊列,即所謂的“宏任務(wù)隊列”(v8術(shù)語):

例如,當(dāng)引擎正忙于執(zhí)行時script,用戶可能會移動鼠標(biāo)mousemove,這=setTimeout可能是由于任務(wù)到期而導(dǎo)致的,等等,這些任務(wù)形成了一個隊列,如上圖所示。

隊列中的任務(wù)將按照“先到先得”的原則進(jìn)行處理。當(dāng)引擎瀏覽器用完成后script,它將處理mousemove事件,然后setTimeout處理程序,依此類推。

到目前為止,很簡單,對吧?

還有兩個細(xì)節(jié):

  1. 引擎執(zhí)行任務(wù)時永遠(yuǎn)不會進(jìn)行渲染。任務(wù)是否花費很長時間都沒關(guān)系。僅在任務(wù)完成后才繪制對DOM的更改。
  2. 如果一項任務(wù)花費的時間太長,瀏覽器將無法執(zhí)行其他任務(wù),例如處理用戶事件。因此,過了一會兒,它會發(fā)出類似“頁面無響應(yīng)”的警報,提示您殺死整個頁面的任務(wù)。當(dāng)存在大量復(fù)雜的計算或?qū)е聼o限循環(huán)的編程錯誤時,就會發(fā)生這種情況。

那是理論?,F(xiàn)在,讓我們看看如何應(yīng)用這些知識。

用例1:拆分占用大量CPU的任務(wù)

假設(shè)我們有一個需要CPU的任務(wù)。

例如,語法高亮(用于著色此頁面上的代碼示例)相當(dāng)占用CPU資源。為了突出顯示代碼,它執(zhí)行分析,創(chuàng)建許多彩色元素,然后將它們添加到文檔中-花費大量時間編寫大量文本。

當(dāng)引擎忙于語法高亮顯示時,它無法執(zhí)行其他與DOM相關(guān)的工作,處理用戶事件等。它甚至可能導(dǎo)致瀏覽器“卡頓”甚至“掛起”一小段時間,這是不可接受的。

通過將大任務(wù)分成多個部分,我們可以避免問題。突出顯示前100行,然后為后100行計劃setTimeout(零延遲),依此類推。

為了簡單起見,為了演示這種方法,讓我們開始一個從11000000000計數(shù)的函數(shù)。

如果您運行下面的代碼,引擎將“掛起”一段時間。對于明顯可見的服務(wù)器端JS,如果您正在瀏覽器中運行它,則嘗試單擊頁面上的其他按鈕-您會發(fā)現(xiàn)在計數(shù)結(jié)束之前不會處理其他事件。

let i = 0;

let start = Date.now();

function count() {

  // do a heavy job
  for (let j = 0; j < 1e9; j++) {
    i++;
  }

  alert("Done in " + (Date.now() - start) + 'ms');
}

count();

瀏覽器甚至可能顯示“腳本花費太長時間”的警告。

讓我們使用嵌套setTimeout調(diào)用拆分作業(yè):

let i = 0;

let start = Date.now();

function count() {

  // do a piece of the heavy job (*)
  do {
    i++;
  } while (i % 1e6 != 0);

  if (i == 1e9) {
    alert("Done in " + (Date.now() - start) + 'ms');
  } else {
    setTimeout(count); // schedule the new call (**)
  }

}

count();

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

一次運行會count完成一部分工作,然后根據(jù)需要重新安排自身的時間,例如:

  1. 首次運行計數(shù):i=1...1000000
  2. 第二次運行計數(shù):i=1000001..2000000。
  3. …等等。

現(xiàn)在,如果onclick在引擎正忙于執(zhí)行第1部分時出現(xiàn)新的輔助任務(wù)(例如事件),則將其排隊,然后在第1部分完成時在下一部分之前執(zhí)行。count執(zhí)行之間定期返回事件循環(huán),為JavaScript引擎提供足夠的“空氣”以執(zhí)行其他操作,以對其他用戶操作做出反應(yīng)。

值得注意的是,兩種變體(無論是否分配工作)setTimeout在速度上都是可比的??傮w計數(shù)時間沒有太大差異。

為了使它們更接近,讓我們進(jìn)行改進(jìn)。

我們將排程移至的開頭count()

let i = 0;

let start = Date.now();

function count() {

  // move the scheduling to the beginning
  if (i < 1e9 - 1e6) {
    setTimeout(count); // schedule the new call
  }

  do {
    i++;
  } while (i % 1e6 != 0);

  if (i == 1e9) {
    alert("Done in " + (Date.now() - start) + 'ms');
  }

}

count();

現(xiàn)在,當(dāng)我們開始count()發(fā)現(xiàn)需要做count()更多的工作時,我們會立即安排工作時間,然后再進(jìn)行這項工作。

如果您運行它,很容易注意到它花費的時間大大減少了。

為什么?

這很簡單:您記得,許多嵌套setTimeout調(diào)用在瀏覽器中的最小延遲為4毫秒。即使我們設(shè)置了0,它4ms(或者更多)。因此,我們計劃得越早–運行速度越快。

最后,我們將需要大量CPU的任務(wù)分成了幾個部分–現(xiàn)在它不會阻塞用戶界面。而且它的整體執(zhí)行時間不會更長。

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

為瀏覽器腳本分配繁重任務(wù)的另一個好處是,我們可以顯示進(jìn)度指示。

如前所述,僅在當(dāng)前運行的任務(wù)完成后才繪制對DOM的更改,而不管它花費多長時間。

一方面,這很棒,因為我們的函數(shù)可能會創(chuàng)建許多元素,將它們一個接一個地添加到文檔中并更改其樣式-訪問者不會看到任何“中間”未完成的狀態(tài)。重要的事情,對不對?

這是演示,在函數(shù)完成之前不會顯示對i的更改,因此我們將僅看到最后一個值:

<div id="progress"></div>

<script>

  function count() {
    for (let i = 0; i < 1e6; i++) {
      i++;
      progress.innerHTML = i;
    }
  }

  count();
</script>

…但是我們也可能希望在任務(wù)執(zhí)行過程中顯示一些東西,例如進(jìn)度條。

如果我們使用來將繁重的任務(wù)分成幾部分setTimeout,則更改將在它們之間繪制出來。

這看起來更漂亮:

<div id="progress"></div>

<script>
  let i = 0;

  function count() {

    // do a piece of the heavy job (*)
    do {
      i++;
      progress.innerHTML = i;
    } while (i % 1e3 != 0);

    if (i < 1e7) {
      setTimeout(count);
    }

  }

  count();
</script>

現(xiàn)在,<div>顯示的是的增加值i,這是一種進(jìn)度條。

用例3:在事件發(fā)生后采取措施

在事件處理程序中,我們可能會決定推遲一些操作,直到事件冒泡并在所有級別上得到處理。我們可以通過將代碼包裝在零延遲setTimeout中來做到這一點。

分派自定義事件一章中,我們看到了一個示例:自定義事件menu-open是在setTimeout中分派的,因此它在完全處理“ click”事件之后發(fā)生。

menu.onclick = function() {
  // ...

  // create a custom event with the clicked menu item data
  let customEvent = new CustomEvent("menu-open", {
    bubbles: true
  });

  // dispatch the custom event asynchronously
  setTimeout(() => menu.dispatchEvent(customEvent));
};

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

微任務(wù)僅來自我們的代碼。它們通常是由Promise創(chuàng)建的:處理程序.then/catch/finally的執(zhí)行成為微任務(wù)。微任務(wù)也被“秘密使用” await,因為它是Promise處理的另一種形式。

還有一個特殊功能queueMicrotask(func),func可在微任務(wù)隊列中排隊等待執(zhí)行。

在每個宏任務(wù)執(zhí)行之后,引擎會立即運行任務(wù)隊列中的所有任務(wù),然后再運行其他宏任務(wù)或渲染或其他任何操作。**

例如,看一下:

setTimeout(() => alert("timeout"));

Promise.resolve()
  .then(() => alert("promise"));

alert("code");

這將是什么順序?

  1. code 首先顯示,因為它是常規(guī)的同步調(diào)用。
  2. promise顯示第二個,因為它.then通過微任務(wù)隊列,并在當(dāng)前代碼之后運行。
  3. timeout 最后顯示,因為它是一個宏任務(wù)。

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


在執(zhí)行任何其他事件處理或呈現(xiàn)或執(zhí)行任何其他宏任務(wù)之前,所有微任務(wù)都已完成。

這很重要,因為它可以確保微任務(wù)之間的應(yīng)用程序環(huán)境基本相同(沒有鼠標(biāo)坐標(biāo)更改,沒有新的網(wǎng)絡(luò)數(shù)據(jù)等)。

如果我們想異步執(zhí)行一個函數(shù)(在當(dāng)前代碼之后),但是在呈現(xiàn)更改或處理新事件之前,可以使用進(jìn)行調(diào)度queueMicrotask

這是一個帶有“計數(shù)進(jìn)度條”的示例,與之前顯示的示例類似,但queueMicrotask用于代替setTimeout。您可以看到它在最后渲染。就像同步代碼一樣:

<div id="progress"></div>

<script>
  let i = 0;

  function count() {

    // do a piece of the heavy job (*)
    do {
      i++;
      progress.innerHTML = i;
    } while (i % 1e3 != 0);

    if (i < 1e6) {
      queueMicrotask(count);
    }

  }

  count();
</script>

概括

更詳細(xì)的事件循環(huán)算法(盡管與規(guī)范相比仍簡化了):

  1. 宏任務(wù)隊列中出隊并運行最早的任務(wù)(例如“腳本”)。
  2. 執(zhí)行所有微任務(wù)
    • 當(dāng)微任務(wù)隊列不為空時:
      • 出隊并運行最早的微任務(wù)。
  3. 渲染更改(如果有)。
  4. 如果宏任務(wù)隊列為空,請等待直到出現(xiàn)宏任務(wù)。
  5. 轉(zhuǎn)到步驟1。

要安排新的宏任務(wù)

  • 使用零延遲setTimeout(f)

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

另外,在事件處理程序中用于安排事件完全處理(冒泡完成)后的操作。

安排新的微任務(wù)

  • 使用queueMicrotask(f)
  • 諾言處理程序還會通過微任務(wù)隊列。

微任務(wù)之間沒有UI或網(wǎng)絡(luò)事件處理:它們立即一個接一個地運行。

因此,您可能想queueMicrotask異步執(zhí)行功能,但要在環(huán)境狀態(tài)下執(zhí)行。

Web Worker
對于不應(yīng)該阻塞事件循環(huán)的長時間繁瑣的計算,我們可以使用Web Workers。
這是在另一個并行線程中運行代碼的方式。
Web Workers可以與主進(jìn)程交換消息,但是它們具有自己的變量和事件循環(huán)。
Web Worker沒有訪問DOM的權(quán)限,因此它們對于同時使用多個CPU內(nèi)核的計算非常有用。

參考

Event loop: microtasks and macrotasks

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容