[譯] 在 JavaScript 中經過 queueMicrotask() 使用微任務

原文:developer.mozilla.org/en-US/docs/…html

一個 微任務(microtask) 就是一個簡短的函數,當建立該函數的函數執行以後,而且 只有當 Javascript 調用棧爲空,而控制權還沒有返還給被 用戶代理 用來驅動腳本執行環境的事件循環以前,該微任務纔會被執行。事件循環既多是瀏覽器的主事件循環也多是被一個 web worker 所驅動的事件循環。這使得給定的函數在沒有其餘腳本執行干擾的狀況下運行,也保證了微任務能在用戶代理有機會對該微服務帶來的行爲作出反應以前運行。web

JavaScript 中的 promisesMutation Observer API 都使用微任務隊列去運行它們的回調函數,但當可以推遲工做直到當前事件循環過程完結時,也是能夠執行微任務的時機。爲了容許第三方庫、框架、polyfills 能使用微任務, Window 暴露了 queueMicrotask() 方法,而 Worker 接口則經過 WindowOrWorkerGlobalScope mixin 提供了同名的 queueMicrotask() 方法。json

任務 vs 微任務

爲了正確地討論微任務,首先最好知道什麼是一個 JavaScript 任務以及微任務如何區別於任務。這裏是一個快速、簡單的解釋,但若你想了解更多細節,能夠閱讀這篇文章中的信息 In depth: Microtasks and the JavaScript runtime environment數組

任務(Tasks)

一個 任務 就是由執行諸如從頭執行一段程序、執行一個事件回調或一個 interval/timeout 被觸發之類的標準機制而被調度的任意 JavaScript 代碼。這些都在 任務隊列(task queue) 上被調度。promise

在如下時機,任務會被添加到任務隊列:瀏覽器

  • 一段新程序或子程序被直接執行時(好比從一個控制檯,或在一個 <script> 元素中運行代碼)。
  • 觸發了一個事件,將其回調函數添加到任務隊列時。
  • 執行到一個由 setTimeout()setInterval() 建立的 timeout 或 interval,以至相應的回調函數被添加到任務隊列時。

事件循環驅動你的代碼按照這些任務排隊的順序,一個接一個地處理它們。在當前迭代輪次中,只有那些當事件循環過程開始時 已經處於任務隊列中 的任務會被執行。其他的任務不得不等待到下一次迭代。緩存

微任務(Microtasks)

起初微任務和任務之間的差別看起來不大。它們很類似;都由位於某個隊列的 JavaScript 代碼組成並在合適的時候運行。可是,只有在迭代開始時隊列中存在的任務纔會被事件循環一個接一個地運行,這和處理微任務隊列是殊爲不一樣的。安全

有兩點關鍵的區別。bash

首先,每當一個任務存在,事件循環都會檢查該任務是否正把控制權交給其餘 JavaScript 代碼。如若否則,事件循環就會運行微任務隊列中的全部微任務。接下來微任務循環會在事件循環的每次迭代中被處理屢次,包括處理完事件和其餘回調以後。服務器

其次,若是一個微任務經過調用 queueMicrotask() 向隊列中加入了更多的微任務,則那些新加入的微任務 會早於下一個任務運行。這是由於事件循環會持續調用微任務直至隊列中沒有留存的,即便是在有更多微任務持續被加入的狀況下。

注意: 由於微任務自身能夠入列更多的微任務,且事件循環會持續處理微任務直至隊列爲空,那麼就存在一種使得事件循環無盡處理微任務的真實風險。如何處理遞歸增長微任務是要謹慎而行的。

使用微任務

在談論更多以前,再次注意到一點是重要的,那就是若是可能的話,大部分開發者並不該該過多的使用微任務。在基於現代瀏覽器的 JavaScript 開發中有一個高度專業化的特性,那就是容許你調度代碼跳轉到其餘事情以前,而那些事情本來是處於用戶計算機中一大堆等待發生的事情集合之中的。濫用這種能力將帶來性能問題。

入列微任務

就其自己而言,應該使用微任務的典型狀況,要麼只有在沒有其餘辦法的時候,要麼是當建立框架或庫時須要使用微任務達成其功能。雖然在過去要使得入列微任務成爲可能有可用的技巧(好比建立一個當即 resolve 的 promise),但新加入的 queueMicrotask() 方法增長了一種標準的方式,能夠安全的引入微任務而避免使用額外的技巧。

經過引入 queueMicrotask(),由晦澀地使用 promise 去建立微任務而帶來的風險就能夠被避免了。舉例來講,當使用 promise 建立微任務時,由回調拋出的異常被報告爲 rejected promises 而不是標準異常。同時,建立和銷燬 promise 帶來了事件和內存方面的額外開銷,這是正確入列微任務的函數應該避免的。

簡單的傳入一個 JavaScript 函數,以在 queueMicrotask() 方法中處理微任務時供其上下文調用便可;取決於當前執行上下文,queueMicrotask() 以定義的形式被暴露在 WindowWorker 接口上。

queueMicrotask(() => {
  /* 微服務中將運行的代碼 */
});

複製代碼

微服務函數自己沒有參數,也不返回值。

什麼時候使用微服務

在本章節中,咱們來看看微服務特別有用的場景。一般,這些場景關乎捕捉或檢查結果、執行清理等;其時機晚於一段 JavaScript 執行上下文主體的退出,但早於任何事件處理函數、timeouts 或 intervals 及其餘回調被執行。

什麼時候是那種有用的時候?

使用微服務的最主要緣由簡單概括爲:確保任務順序的一致性,即使當結果或數據是同步可用的,也要同時減小操做中用戶可感知到的延遲而帶來的風險。

保證條件性使用 promises 時的順序

微服務可被用來確保執行順序老是一致的一種情形,是當 promise 被用在一個 if...else 語句(或其餘條件性語句)中、但並不在其餘子句中的時候。考慮以下代碼:

customElement.prototype.getData = url => {
  if (this.cache[url]) {
    this.data = this.cache[url];
    this.dispatchEvent(new Event("load"));
  } else {
    fetch(url).then(result => result.arrayBuffer()).then(data => {
      this.cache[url] = data;
      this.data = data;
      this.dispatchEvent(new Event("load"));
    )};
  }
};
複製代碼

這段代碼帶來的問題是,經過在 if...else 語句的其中一個分支(此例中爲緩存中的圖片地址可用時)中使用一個任務而 promise 包含在 else 子句中,咱們面臨了操做順序可能不一樣的局勢;比方說,像下面看起來的這樣:

element.addEventListener("load", () => console.log("Loaded data"));
console.log("Fetching data...");
element.getData();
console.log("Data fetched");

複製代碼

連續執行兩次這段代碼會造成下表中的結果:

數據未緩存的結果(左) vs. 緩存中有數據的結果
數據未緩存 數據已緩存
Fetching data
Data fetched
Loaded data
Fetching data
Loaded data
Data fetched

甚至更糟的是,有時元素的 data 屬性會被設置,還有時當這段代碼結束運行時卻不會被設置。

咱們能夠經過在 if 子句裏使用一個微任務來確保操做順序的一致性,以達到平衡兩個子句的目的:

customElement.prototype.getData = url => {
  if (this.cache[url]) {
    queueMicrotask(() => {
      this.data = this.cache[url];
      this.dispatchEvent(new Event("load"));
    });
  } else {
    fetch(url).then(result => result.arrayBuffer()).then(data => {
      this.cache[url] = data;
      this.data = data;
      this.dispatchEvent(new Event("load"));
    )};
  }
};
複製代碼

經過在兩種狀況下各自都經過一個微任務(if 中用的是 queueMicrotask()else 子句中經過 fetch() 使用了 promise)處理了設置 data 和觸發 load 事件,平衡了兩個子句。

批量操做

也可使用微任務從不一樣來源將多個請求收集到單一的批處理中,從而避免對處理同類工做的屢次調用可能形成的開銷。

下面的代碼片斷建立了一個函數,將多個消息放入一個數組中批處理,經過一個微任務在上下文退出時將這些消息做爲單一的對象發送出去。

const messageQueue = [];

let sendMessage = message => {
  messageQueue.push(message);

  if (messageQueue.length === 1) {
    queueMicrotask(() => {
      const json = JSON.stringify(messageQueue);
      messageQueue.length = 0;
      fetch("url-of-receiver", json);
    });
  }
};

複製代碼

sendMessage() 被調用時,指定的消息首先被推入消息隊列數組。接着事情就變得有趣了。

若是咱們剛加入數組的消息是第一條,就入列一個將會發送一個批處理的微任務。照舊,當 JavaScript 執行路徑到達頂層,恰在運行回調以前,那個微任務將會執行。這意味着以後的間歇期內形成的對 sendMessage() 的任何調用都會將其各自的消息推入消息隊列,但囿於入列微任務邏輯以前的數組長度檢查,不會有新的微任務入列。

當微任務運行之時,等待它處理的多是一個有若干條消息的數組。微任務函數先是經過 JSON.stringify() 方法將消息數組編碼爲 JSON。其後,數組中的內容就再也不須要了,因此清空 messageQueue 數組。最後,使用 fetch() 方法將編碼後的 JSON 發往服務器。

這使得同一次事件循環迭代期間發生的每次 sendMessage() 調用將其消息添加到同一個 fetch() 操做中,而不會讓諸如 timeouts 等其餘可能的定時任務推遲傳遞。

服務器將接到 JSON 字符串,而後大概會將其解碼並處理其從結果數組中找到的消息。

例子

簡單微任務示例

在這個簡單的例子中,咱們將看到入列一個微任務後,會引發其回調函數在頂層腳本完畢後運行。

HTML

<pre id="log">
</pre>
複製代碼

JavaScript

如下代碼用於記錄輸出。

let logElem = document.getElementById("log");
let log = s => logElem.innerHTML += s + "<br>";
複製代碼

在下面的代碼中,咱們看到對 queueMicrotask() 的一次調用被用來調度一個微任務以使其運行。此次調用包含了 log(),一個簡單的向屏幕輸出文字的自定義函數。

log("Before enqueueing the microtask");
queueMicrotask(() => {
  log("The microtask has run.")
});
log("After enqueueing the microtask");
複製代碼

結果

Before enqueueing the microtask
After enqueueing the microtask
The microtask has run.
複製代碼

timeout 和微任務的示例

在這個例子中,一個 timeout 在 0 毫秒後被觸發(或者 "儘量快")。這演示了當調用一個新任務(如經過使用 setTimeout())時的「儘量快」意味着什麼,以及比之於使用一個微任務的不一樣。

HTML

<pre id="log">
</pre>
複製代碼

JavaScript

如下代碼用於記錄輸出。

let logElem = document.getElementById("log");
let log = s => logElem.innerHTML += s + "<br>";
複製代碼

在下面的代碼中,咱們看到對 queueMicrotask() 的一次調用被用來調度一個微任務以使其運行。此次調用包含了 log(),一個簡單的向屏幕輸出文字的自定義函數。

如下代碼調度了一個 0 毫秒後觸發的 timeout,然後入列了一個微任務。先後被對 log() 的調用包住,輸出附加的信息。

let callback = () => log("Regular timeout callback has run");

let urgentCallback = () => log("*** Oh noes! An urgent callback has run!");

log("Main program started");
setTimeout(callback, 0);
queueMicrotask(urgentCallback);
log("Main program exiting");
複製代碼

結果

Main program started
Main program exiting
*** Oh noes! An urgent callback has run!
Regular timeout callback has run
複製代碼

能夠注意到,從主程序體中輸出的日誌首先出現,接下來是微任務中的輸出,其後是 timeout 的回調。這是由於當處理主程序運行的任務退出後,微任務隊列先於 timeout 回調所在的任務隊列被處理。要記住任務和微任務是保持各自獨立的隊列的,且微任務先執行有助於保持這一點。

來自函數的微任務

這個例子經過增長一個完成一樣工做的函數,略微地擴展了前一個例子。該函數使用 queueMicrotask() 調度一個微任務。此例的重要之處是微任務不在其所處的函數退出時,而是在主程序退出時被執行。

HTML

<pre id="log">
</pre>
複製代碼

JavaScript

如下代碼用於記錄輸出。

let logElem = document.getElementById("log");
let log = s => logElem.innerHTML += s + "<br>";
複製代碼

如下是主程序代碼。這裏的 doWork() 函數調用了 queueMicrotask(),但微任務仍在整個程序退出時才觸發,由於那纔是任務退出而執行棧上爲空的時刻。

let callback = () => log("Regular timeout callback has run");

let urgentCallback = () => log("*** Oh noes! An urgent callback has run!");

let doWork = () => {
  let result = 1;

  queueMicrotask(urgentCallback);

  for (let i=2; i<=10; i++) {
    result *= i;
  }
  return result;
};

log("Main program started");
setTimeout(callback, 0);
log(`10! equals ${doWork()}`);
log("Main program exiting");
複製代碼

結果

Main program started
10! equals 3628800
Main program exiting
*** Oh noes! An urgent callback has run!
Regular timeout callback has run
複製代碼


--End--

搜索 fewelife 關注公衆號

轉載請註明出處

相關文章
相關標籤/搜索