本文由 IMWeb 团队成员 shijisun 首发于 IMWeb 社区网站 imweb.io。点击阅读原文查看 IMWeb 社区更多精彩文章。
本文有标题党之嫌,内含大量Microtaks相关总结性信息,请谨慎服用。
2018年9月21日,虽然没有参加该场GDD,但是也有幸拜读了百度@小蘑菇小哥总结的文章深入浏览器的事件循环(GDD@2018),配注的说明插图形象生动,文终的click代码也很有意思,推荐大家阅读。这里就先恬不知耻的将该文的精华以及一些自己的总结陈列如下:

异步任务 | 特点 | 常见产生处 |
|---|---|---|
Tasks (Macrotasks) | - 当次事件循环执行队列内的一个任务- 当次事件循环产生的新任务会在指定时机加入任务队列等待执行 | - setTimeout- setInterval- setImmediate- I/O |
Animation callbacks | - 渲染过程(Structure-Layout-Paint)前执行- 当次事件循环执行队列里的所有任务- 当次事件循环产生的新任务会在下一次循环执行 | - rAF |
Microtasks | - 当次事件循环的结尾立即执行的任务- 当次事件循环执行队列里的所有任务- 当次事件循环产生的新任务会立即执行 | - Promise- Object.observe- MutationObserver- process.nextTick |
看过一篇公众号文章下面的留言:
那个所谓的mtask和task的区别我并不认同...,我认为事件对列只有一个,就是task。
特别是对于JS异步编程思维还不太熟悉的同学,比如两年前从java转成javascript后的我,对于这种异步的调用顺序其实很难理解。
不过有一个特别能说明Macrotasks和Microtasks的例子:
// 普通的递归, 造成死循环, 页面无响应function callback() { console.log('callback'); callback();}callback();上面的代码相信大家非常好理解,一个很简单的递归,由于事件循环得不到释放,UI渲染无法进行导致页面无响应。
通常我们可以使用setTimeout来进行改造,我们把下一次执行放到异步队列里面,不会持久的占用计算资源,这就是我们说的Macrotasks:
// 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的相关疑虑 (~~特别是有人拿出一段参杂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+的相关注解内容:
Here “platform code” means engine,environment,and promise implementation code. In practice,this requirement ensures that
onFulfilledandonRejectedexecute asynchronously,after the event loop turn in whichthenis called,and with a fresh stack. This can be implemented with either a “macro-task” mechanism such assetTimeoutorsetImmediate,or with a “micro-task” mechanism such asMutationObserverorprocess.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的 onFulfilled 和 onRejected 回调的执行只要确保是在 then被调用后异步执行就可以了。具体实现成 setTimeout 似的 macrotasks 机制或者 process.nextTick 似的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,执行顺序可以总结如下:
从这个方面我们也可以理解为什么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}社区官网:http://imweb.io/
招聘帖:https://hr.tencent.com/position_detail.php?id=26701

扫码关注 IMWeb前端社区 公众号,获取最新前端好文
微博、掘金、Github、知乎可搜索 IMWeb 或 IMWeb团队 关注我们。
👇点击阅读原文获取更多参考资料