[译] 在 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 关注公众号

转载请注明出处

相关文章
相关标签/搜索