queueMicrotask 来执行微任务,Window 或 Worker 接口的 queueMicrotask() 方法,浏览器兼容性如下图所示。
在不支持 queueMicrotask 方法的浏览器里面,我们需要使用 polyfill 模拟实现。
下面的代码是一份 queueMicrotask() 的 polyfill。它通过使用立即 resolve 的 promise 创建一个微任务(microtask),如果无法创建 promise,则回落(fallback)到使用setTimeout()。
if (typeof window.queueMicrotask !== "function") {
window.queueMicrotask = function (callback) {
Promise.resolve()
.then(callback)
.catch(e => setTimeout(() => { throw e; }));
};
}
为什么我们需要这个 api?
从微任务本身的概念来说的话,就是当我们期望某段代码,不阻塞当前执行的同步代码,同时又期望它尽可能快地执行时,我们就需要它。
一般情况下,如果是编写业务代码,我觉得很少会遇到这样的需求,唯一能想到的情况可能存在于一些对即时反馈有性能要求的场景,比如搜索,当输入关键字后发送异步请求获取搜索信息之后,我们可能会在前端对搜索结果进行一些处理,比如排序或者分组,但是这些操作可能不是优先级最高的任务,但它们又比较耗时(比如排序),因此我们可能期望推迟它们的执行,但又期望它们尽可能早地执行。
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');
});
会得到和上面代码一样的运行结果。
因为微任务自身可以入列更多的微任务,且事件循环会持续处理微任务直至队列为空,那么就存在一种使得事件循环无尽处理微任务的真实风险。如何处理递归增加微任务是要谨慎而行的。
如果可能的话,大部分开发者并不应该过多的使用微任务。在基于现代浏览器的 JavaScript 开发中有一个高度专业化的特性,那就是允许你调度代码跳转到其他事情之前,而那些事情原本是处于用户计算机中一大堆等待发生的事情集合之中的。滥用这种能力将带来性能问题。
通过引入 queueMicrotask(),可以避免通过 promise 去创建微任务而带来的风险。举例来说,当使用 promise 创建微任务时,由回调抛出的异常被报告为 rejected promises 而不是标准异常。同时,创建和销毁 promise 带来了事件和内存方面的额外开销,这是正确入列微任务的函数应该避免的。