共计 2251 个字符,预计需要花费 6 分钟才能阅读完成。
写在前面
写这篇文章的原因是因为,这几天在看 core-js
的源码,然后发现了 queueMicrotask
的实现。由于之前做的项目,对于微任务的执行需求,一般是使用 asap
这个库来完成的,如果没有使用这个库的话,简易版本可以通过 Promise.resolve()
来代替,并没有接触过过这个 api
,所以就抽时间研究一下。
兼容性
一般看这种偏 web 标准的新的 api
,肯定上来要先看兼容性的,我去 caniuse
查了一下,wtf? 竟然搜索无结果。(详见 issue)
然后只能去 MDN 来看一下了,大概是下图这个样子:
可以发现还是比较新的 api
,如果要在项目中直接使用的话,还是建议导入 polyfill
或者使用 asap
这个库来实现类似的需求。
为什么我们需要这个 api
?
从微任务本身的概念来说的话,就是当我们期望某段代码,不阻塞当前执行的同步代码,同时又期望它尽可能快地执行时,我们就需要它(这里不再赘述微任务的概念,可以参考这篇文章)。
一般情况下,如果是编写业务代码,我觉的很少会遇到这样的需求,唯一能想到的情况可能存在于一些对即时反馈有性能要求的场景,比如搜索,当输入关键字后发送异步请求获取搜索信息之后,我们可能会在前端对搜索结果进行一些处理,比如排序或者分组,但是这些操作可能不是优先级最高的任务,但它们又比较耗时(比如排序),因此我们可能期望推迟它们的执行,但又期望它们尽可能早地执行。
在阅读一些著名框架或者工具库的过程中,我发现很多情况下作者都会遇到这个需求,一般都通过 process.nextTick
或者 Promise.resolve
来解决。
它和 setTimeout
的区别?
本质上的区别应该在它们的执行时机上,而执行时机上的区别,本质上就是微任务和宏任务的区别。可以直接打开控制台运行一下以下的代码:
setTimeout(() => {console.log('setTimeout');
}, 0);
queueMicrotask(() => {console.log('queueMicrotask');
});
运行结果不出意外应该是:
queueMicrotask
setTimeout
如果你熟悉 nodejs
的话,应该和 process.nextTick
是类似的。
使用其他方式进行模拟所带来的问题?
这也是我一开始脑海中出现的问题,就是既然我们已经可以通过别的方式来模拟微任务的执行,我们还需要这个 api
干什么?比如,通过下面的代码:
setTimeout(() => {console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {console.log('queueMicrotask');
});
会得到和上面代码一样的运行结果。
这里引用 Explainer: queueMicrotask 的一些观点来进行阐述:
- 我们应当使用底层 api 来直接完成类似的功能,而非用顶层 api 进行模拟
- 模拟过程中,对于异常情况,会造成一些困扰,比如
Promise.resolve
会将异常转化为一个rejected
的Promise
- 模拟过程中,会创建额外的对象(造成一定意义上的浪费),比如
Promise.resolve
会返回一个Promise
实例对象,而直接queueMicrotask
则不会 - 除了微任务,其他类型的异步任务都有对应的
api
可供使用,比如宏任务、RAF
-
继上一点的基础上,语义性会更好,同时帮助开发者理解这些不同异步任务之间的区别
-
setTimeout(callback, 0)
– 宏任务 -
requestAnimationFrame(callback)
– RAF -
queueMicrotask(callback)
– 微任务
-
潜在问题
由于它是一个用于指派微任务的底层 api
,我们很可能会在其中无限制地指派微任务到其队列之中,这样做的效果就是,浏览器的微任务队列始终处于非空状态,这将导致控制权 始终 无法交还给浏览器进行下一次事件循环,然后它就卡死了。
你可以执行下面的代码来体验这个现象:
function infiniteEnqueue(fn) {queueMicrotask(() => infiniteEnqueue(fn))
}
infiniteEnqueue(()=>{})
执行这段代码会使浏览器当前的 tab
卡死,请慎用,建议先打开浏览器提供的进程管理窗口以供强制关闭卡死窗口。
关于 polyfill 的不同实现
这里简单阐述 MDN
上的和 core-js
中的模拟方案。
MDN
MDN 上的 polyfill 实现比较简单粗暴,其实和直接调用 Promise.resolve
没什么区别,只是会在 .catch
中捕获错误之后再抛出。
core-js
相比较 MDN 的实现,core-js
会复杂一些,它同时考虑了 nodejs
和 browser
两种情况,同时利用链表数据结构来模拟微任务队列的执行单元,同时实现了一个 flush
方法表示执行全部的微任务单元。
还实现了一个 notify
方法,该方法会根据具体的 js
运行时环境以及 api
的支持情况,分别尝试使用 process.nextTick
、MutationObserver
和 Promise.resolve
以及最基本的宏任务 api
来执行 flush
方法,变相模拟微任务的执行过程。
参考
- Explainer: queueMicrotask
- introduction to microtasks
- MDN
关注公众号 全栈_101,只谈技术,不谈人生。
另:本人最近比较缺钱,业余时间接手各种规模的全栈外包项目或者开发任务,有意者私聊。