我与 Microtasks 的前世此生之一眼望穿千年

本文由 IMWeb 团队成员 shijisun 首发于 IMWeb 社区网站 imweb.io。点击阅读原文查看 IMWeb 社区更多精彩文章。javascript

本文有标题党之嫌,内含大量Microtaks相关总结性信息,请谨慎服用。
java

Google Developer Day China 2018 by Jake Archibald

2018年9月21日,虽然没有参加该场GDD,可是也有幸拜读了百度@小蘑菇小哥总结的文章深刻浏览器的事件循环(GDD@2018),配注的说明插图形象生动,文终的click代码也颇有意思,推荐你们阅读。这里就先恬不知耻的将该文的精华以及一些本身的总结陈列以下:react

图片

异步任务 特色 常见产生处
Tasks (Macrotasks) - 当次事件循环执行队列内的一个任务
- 当次事件循环产生的新任务会在指定时机加入任务队列等待执行
- setTimeout
- setInterval
- setImmediate
- I/O
Animation callbacks - 渲染过程(Structure-Layout-Paint)前执行
- 当次事件循环执行队列里的全部任务
- 当次事件循环产生的新任务会在下一次循环执行
- rAF
Microtasks - 当次事件循环的结尾当即执行的任务
- 当次事件循环执行队列里的全部任务
- 当次事件循环产生的新任务会当即执行
- Promise
- Object.observe
- MutationObserver
- process.nextTick

直观的感觉一下 Macrotasks 和 Microtasks

看过一篇公众号文章下面的留言:web

那个所谓的mtask和task的区别我并不认同...,我认为事件对列只有一个,就是task。编程

特别是对于JS异步编程思惟还不太熟悉的同窗,好比两年前从java转成javascript后的我,对于这种异步的调用顺序其实很难理解。promise

不过有一个特别能说明Macrotasks和Microtasks的例子:浏览器

// 普通的递归, 形成死循环, 页面无响应function callback() {    console.log('callback');    callback();}callback();

上面的代码相信你们很是好理解,一个很简单的递归,因为事件循环得不到释放,UI渲染没法进行致使页面无响应。缓存

一般咱们可使用setTimeout来进行改造,咱们把下一次执行放到异步队列里面,不会持久的占用计算资源,这就是咱们说的Macrotasks:app

// Macrotasks,不会形成死循环function callback() {  console.log('callback');  setTimeout(callback,0);}callback();

可是Promise回调产生的Microtasks呢,以下代码,一样会形成死循环。异步

经过上文咱们也能够知道当次事件循环产生的新Microtasks会当即执行,同时当次事件循环要等到全部Microtasks队列执行完毕后才会结束。因此当咱们的Microtasks在产生新的任务的同时,会致使Microtasks队列一直有任务等待执行,此次事件循环永远不会退出,也就致使了咱们的死循环。

// Microtasks,一样会形成死循环,页面无响应function callback() {  console.log('callback');  Promise.resolve().then(callback);}callback();

Microtasks 与 Promise A+

固然,上文解决了本人关于Microtasks的相关疑虑 (~~特别是有人拿出一段参杂setTimeout和Promise的代码让你看代码输出顺序时~~) 的同时,也让我回忆起彷佛曾几什么时候也在哪里看到过关于Microtask的字眼。

通过多日的寻找,终于在之前写过的一片关于Promise的总结文章 打开Promise的正确姿式 里找到了。该文经过一个实例说明了新建Promise的代码是会当即执行的,并不会放到异步队列里:

var d = new Date();// 建立一个promise实例,该实例在2秒后进入fulfilled状态var promise1 = new Promise(function (resolve,reject) {  setTimeout(resolve,2000,'resolve from promise 1');});// 建立一个promise实例,该实例在1秒后进入fulfilled状态var promise2 = new Promise(function (resolve,reject) {  setTimeout(resolve,1000,promise1); // resolve(promise1)});promise2.then(  result => console.log('result:',result,new Date() - d),  error => console.log('error:',error))

上面的代码输出

result: resolve from promise 1 2002

咱们获得两点结论:

  • 验证了Promise/A+中的2.3.2规范

  • 新建Promise的代码时会当即执行的 (运行时间是2秒而不是3秒)

可是当时本人忽略了Promise/A+的相关注解内容:

Here “platform code” means engine,environment,and promise implementation code. In practice,this requirement ensures that onFulfilled and onRejected execute asynchronously,after the event loop turn in which then is called,and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeoutor setImmediate,or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code,it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.

是的,这就是本人与MicroTasks的第一次相遇,没有一见倾心还真是很是抱歉啊。

该注解说明了Promise的 onFulfilledonRejected 回调的执行只要确保是在 then被调用后异步执行就能够了。具体实现成 setTimeout 似的 macrotasks 机制或者 process.nextTick 似的microtasks机制均可以,具体视平台代码而定。

为何须要Microtasks

搜索引擎能找到的相关文章基本都指向了一篇《Tasks,microtasks,queues and schedules》,也许这就是传说中原罪的发源之地吧。

Microtasks are usually scheduled for things that should happen straight after the currently executing script,such as reacting to a batch of actions,or to make something async without taking the penalty of a whole new task.

简单来讲,就是但愿对一系列的任务作出回应或者执行异步操做,可是又不想额外付出一整个异步任务的代价。在这种状况下,Microtasks就能够用来调度这些应当在当前执行脚本结束后立马执行的任务

The microtask queue is processed after callbacks as long as no other JavaScript is mid-execution,and at the end of each task. Any additional microtasks queued during microtasks are added to the end of the queue and also processed.

单独看Macrotasks和 Microtasks,执行顺序能够总结以下:

  • 取出Macrotasks任务队列的一个任务,执行;

  • 取出Microtasks任务队列的全部任务,依次执行;

  • 本次事件循环结束,等待下次事件循环;

从这个方面咱们也能够理解为何Promise.then要被实现成Microtasks,回调在实现Promise/A+规范 (必须是异步执行)的基础上,也保证可以更快的被执行,而不是跟Macrotasks同样必须等到下次事件循环才能执行。你们能够从新执行一下上文对比Macrotasks和Microtasks时举的例子,也会发现他们两的单位时间内的执行次数是不同的。

能够试想一些综合了异步任务和同步任务的的Promise实例,Microtasks能够保证它们更快的获得执行资源,例如:

new Promise((resolve) => {  if(/* 检查资源是否须要异步加载 */) {    return asyncAction().then(resolve);  }  // 直接返回加载好的异步资源  return syncResource;});

若是上面的代码是为了加载远程的资源,那么只有第一次须要执行异步加载,后面的全部执行均可以直接同步读取缓存内容。若是使用Microtasks,咱们也就不用每次都等待多一次的事件循环来获取该资源,Promise实例的新建过程是当即执行的,同时 onFulfilled回调也是在本次事件循环中所有执行完毕的,减小了切换上下文的成本,提升了性能。

可是呢,从上文关于Promise/A+规范的引用中咱们已经知道不一样浏览器对于该实现是不一致的。部分浏览器 (愈来愈少) 将Promise的回调函数实现成了Macrotasks,缘由就在于Promise的定义来自ECMAScript而不是HTML。

A Job is an abstract operation that initiates an ECMAScript computation when no other ECMAScript computation is currently in progress. A Job abstract operation may be defined to accept an arbitrary set of job parameters.

按照ECMAScript的规范,是没有Microtasks的相关定义的,相似的有一个 jobs的概念,和Microtasks很类似.

相关应用

Vue - src/core/utils/next-tick.js 中也有相关Macrotask和Microtask的实现

let microTimerFunclet macroTimerFuncif (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {  macroTimerFunc = () => {    setImmediate(flushCallbacks)  }} else if (typeof MessageChannel !== 'undefined' && (  isNative(MessageChannel) ||  // PhantomJS  MessageChannel.toString() === '[object MessageChannelConstructor]')) {  const channel = new MessageChannel()  const port = channel.port2  channel.port1.onmessage = flushCallbacks  macroTimerFunc = () => {    port.postMessage(1)  }} else {  /* istanbul ignore next */  macroTimerFunc = () => {    setTimeout(flushCallbacks,0)  }}// Determine microtask defer implementation./* istanbul ignore next,$flow-disable-line */if (typeof Promise !== 'undefined' && isNative(Promise)) {  const p = Promise.resolve()  microTimerFunc = () => {    p.then(flushCallbacks)    // in problematic UIWebViews,Promise.then doesn't completely break,but    // it can get stuck in a weird state where callbacks are pushed into the    // microtask queue but the queue isn't being flushed,until the browser    // needs to do some other work,e.g. handle a timer. Therefore we can    // "force" the microtask queue to be flushed by adding an empty timer.    if (isIOS) setTimeout(noop)  }} else {  // fallback to macro  microTimerFunc = macroTimerFunc}
相关文章
相关标签/搜索