FrankKai / FrankKai.github.io

FE blog
https://frankkai.github.io/
362 stars 39 forks source link

[译]如何理解任务(task)和微任务(microtask)? #218

Open FrankKai opened 4 years ago

FrankKai commented 4 years ago

初识任务和微任务

什么是微任务?在什么之后执行?在什么之前执行?为什么要这样设计?为什么要这样设计?

microtask是一个短函数。 microtask在创建它的函数或者程序退出之后执行,并且只有在JavaScript执行栈为空时调用。 microtask在将控制权交给用户代理驱动JavaScript执行环境的event loop之前。 这个event loop可以是浏览器的主event loop,也可以是驱动一个web worker的event loop。 这样设计是为了使得指定的函数在不干扰其他代码的情况下执行,同时还确保微任务在用户代理有机会对微任务所采取的操作做出反应之前运行。

常见的微任务队列有哪些?

JavaScript中的promiseMutation Observer都使用了微任务队列去运行他们的callback,但是在有些情况下,也可以将work延迟到当前event loop结束时。 为了允许微任务能被第三方库,框架和polyfill使用,queueMicrotask()方法通过WindowOrWorkerGlobalScope暴露给了Window和Worker interface。

任务 vs 微任务

为了更加正确的讨论microtask,首先要知道JavaScript中的task是什么,然后才能知道它和microtask的区别。 这里我们仅仅罗列简单的对比,如果想要了解任务的更多细节,可以去阅读In depth: Microtasks and the JavaScript runtime environment.

如何理解任务?

任务(task)指的是由标准的机制调度运行的任意JavaScript代码。 比如开始运行一个程序,一个运行的事件回调,或者一个interval或timeout的执行。 这些会被调度到task queue

任务符合以下条件的话可以被添加到任务队列:

event loop会把我们的代码一个接一个的去处理,并且是按照入队的顺序去处理。 在事件循环开始时,只有已经在任务度列中的任务才会在当前迭代期间执行。 其他的需要等待下一个迭代。

如何理解微任务?

microtask和task看起来区别很小。 它们的相同点为:二者都会使JavaScript代码进入到队列,并且在合适的时间运行。 然而,在循环开始时,事件循环仅仅运行在队列中的任务,处理微任务是非常不同的。

主要有以下两个区别:

如何使用微任务?

在深入微任务之前,需要强调的一点是,大多数开发都不会用到微任务。 它们基于现代浏览器的JavaScript开发的高度专门化的特性,使得你可以在等待用户计算机等待一些长任务之前调度代码。 滥用这个特性可能会导致性能问题。

队列微任务

只有在没有别的解决方案的情况下,再去用微任务,或者是用微任务创建框架或者库。 过去有一些trick去实现入队(比如通过创建一个立即resolve的promise),现在queueMicrotask()方法是一个标准的添加微任务的方式。

通过使用queueMicrotask(),使用promise出现的问题是可以避免的。因为通过promise创建的微任务的话,callback的异常不是标准的异常。此外,创建和销毁Promise在时间和内存方面都会有额外的开销,使用queueMicrotask()可以避免。

可以直接向queueMicrotask()传入一个函数,用来处理微任务。 可以是Window的,也可以是Worker的。

queueMicrotask(()=>{
    // 这里是运行微任务的代码
})

微任务本身不需要任何参数,也不需要返回任何值。

什么时候使用微任务

**微任务特别有用的场景。通常,它是在JavaScript执行上下文主体退出后,但在处理任何事件处理程序、setTimeout和setInterval或其他回调之前捕获或检查结果,或执行清理。

使用微任务的主要原因很简单:确保任务的顺序一致,即使结果或数据是同步可用的,但同时降低了用户可识别的操作延迟的风险。

使用Promise确保条件判断排序

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"));
    });
  }
};
element.addEventListener("load", () => console.log("Loaded data"));
console.log("Fetching data...");
element.getData();
console.log("Data fetched");

缓存与不缓存的对比: image

代码的执行顺序是无法得到保证的。

可以用queueMicrotask()去解决这个问题。 同步变异步。在call stack清空后,下一个event loop开始前执行。

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

避免发送重复请求

可以用microtask收集很多请求到一起,避免同一个任务多次调用。 下面的代码创建了一个函数并且将多个message收集到数组中,使用微任务发请求:

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()操作中,而不会潜在地使其他任务(例如超时等)延迟传输。

服务器将接收JSON字符串,然后大概对其进行解码并处理在结果数组中找到的消息。

使用微任务的例子

最简微任务例子

queueMicrotask中的回调在top-level函数调用栈完成时再调用。

console.log("Before enqueueing the microtask");
queueMicrotask(() => {
  console.log("The microtask has run.")
});
console.log("After enqueueing the microtask");

结果: Before enqueueing the microtask After enqueueing the microtask The microtask has run.

Timeout和微任务例子

优先级: 微任务队列 > 任务队列

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

这样的结果是因为:主程序的任务执行完成后,microtask queue先处理,然后处理timeout所在的task queue。牢记微任务和任务是放置在不同的队列中的,微任务首先执行。

函数中的微任务

微任务在函数退出时不会调用,它只会在main program退出时调用,也就是call stack清空时调用。

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");

参考资料:https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide