宏任务和微任务:不是所有的任务都是一个待遇|浏览器篇
# 宏任务和微任务:不是所有的任务都是一个待遇
30 秒速记
- 浏览器消息循环处理来自
setTimeout、XMLHttpRequest等异步来源的任务,但单一任务粒度无法覆盖所有时效需求。 - 微任务提供了更细的调度层级,用于在当前任务结束后及时处理后续工作。
Promise和MutationObserver是原文列出的微任务相关机制,许多基于Promise的技术也继承这种调度特征。- 宏任务与微任务的关键差异在调度时机,而不是业务重要程度;二者共同服务于浏览器事件循环。
- 微任务提升及时性也有边界:本题原文只引出其必要性,未给出完整队列执行规则与饥饿问题,回答时不应据此扩展实现细节。
宏任务和微任务本质上都是事件循环中的任务,区别主要在调度时机,而不是谁在业务上更重要。 像 setTimeout、XMLHttpRequest 这类异步来源依赖消息循环,但这种任务粒度较粗,难以兼顾部分场景对及时性的要求。微任务会在当前任务结束后更及时地处理后续工作,Promise 和 MutationObserver 都采用了这类机制。它是在实时性和效率之间做权衡,但这里只能说明它为什么出现,不能据此扩展完整的队列规则或饥饿问题。
在前面几篇文章中,我们介绍了消息队列,并结合消息队列介绍了两种典型的 WebAPI——setTimeout和XMLHttpRequest,通过这两个 WebAPI 我们搞清楚了浏览器的消息循环系统是怎么工作的。不过随着浏览器的应用领域越来越广泛,消息队列中这种粗时间颗粒度的任务已经不能胜任部分领域的需求,所以又出现了一种新的技术——微任务。微任务可以在实时性和效率之间做一个有效的权衡。
从目前的情况来看,微任务已经被广泛地应用,基于微任务的技术有 MutationObserver、Promise 以及以 Promise 为基础开发出来的很多其他的技术。所以微任务的重要性也与日俱增,了解其底层的工作原理对于你读懂别人的代码,以及写出更高效、更具现代的代码有着决定性的作用。
有微任务,也就有宏任务,那这二者到底有什么区别?它们又是如何相互取长补短的?
面试官追问
追问 1代码评审中有人说“登录回调更重要,所以应该放进微任务;埋点不重要,就放进宏任务”,你会怎样纠正这种分类?
不能按业务重要程度划分宏任务和微任务,二者区别在调度机制与执行时机。Promise、MutationObserver 采用微任务,而 setTimeout、网络完成等进入消息队列;业务优先级仍需通过具体流程设计表达。
追问 2搜索框输入后既要更新提示状态,又要等待接口返回;团队想把所有后续逻辑都换成微任务来提升实时性,你会同意吗?
不应把所有异步逻辑都塞进微任务,只有需要在当前任务结束前及时处理的工作才适合这种调度。网络完成本身仍由事件循环安排,微任务也不能缩短请求耗时;回调过重还会推迟渲染与下一轮交互。
追问 3原本只更新一次状态的页面改成批量导入,单次操作会产生上百个后续回调;继续优先使用微任务会带来什么约束?
规模扩大后必须控制每个微任务的工作量,以及执行期间是否继续产生新微任务。微任务会在检查点持续执行到队列清空,数量和耗时都会延长当前宏任务;实时性优势可能反过来造成页面迟迟无法渲染。
追问 4线上偶现 setTimeout(..., 0) 比预期晚执行,而同一段代码里的 Promise.then 已经完成,你会如何定位而不是直接判断浏览器异常?
应先用性能记录查看当前脚本、渲染、交互和其他消息队列任务是否占用了主线程,并确认微任务队列是否过长。定时器属于后续宏任务,无法精确控制入队位置和执行时间;零延迟只表示尽早调度,不保证立即运行。
追问 5富文本编辑器要监听连续 DOM 改动,架构师在高频轮询和 MutationObserver 之间选型,你会依据什么做决定?
连续 DOM 变化更适合 MutationObserver,它通过微任务在及时性和效率之间取得平衡,避免轮询间隔过长或无效检查过多。回调仍应批量处理变化记录并限制计算量,否则微任务本身也会拖长当前任务。
# 宏任务
30 秒速记
- 宏任务是事件循环从消息队列中选取并交给主线程执行的任务,脚本、用户交互、渲染及部分异步回调都可能以此方式被调度。
- 一次调度会选取可执行队列中较早的任务,将其标记为当前任务,执行完成后移出队列并记录耗时。
setTimeout只保证回调在延迟条件满足后进入可调度状态,不保证在指定时刻立即运行。- 多个宏任务之间可能穿插渲染、交互或其他系统任务,业务代码无法精确决定回调在队列中的位置。
- 宏任务适合一般异步调度,但时间粒度较粗;队列拥堵或前序任务耗时过长时,后续回调会明显延迟。
宏任务就是事件循环从消息队列中取出并交给主线程执行的一类任务。 脚本、用户交互、渲染以及部分异步回调都可能按这种方式调度,任务执行完后才会从对应队列移除。setTimeout 只表示延迟条件满足后可以参与调度,并不保证到点立刻执行。因为两个宏任务之间可能插入渲染或系统任务,所以队列拥堵或前序任务过重时,回调会明显延后,不适合要求高时间精度的场景。
前面我们已经介绍过了,页面中的大部分任务都是在主线程上执行的,这些任务包括了:
- 渲染事件(如解析 DOM、计算布局、绘制);
- 用户交互事件(如鼠标点击、滚动页面、放大缩小等);
- JavaScript 脚本执行事件;
- 网络请求完成、文件读写完成事件。
为了协调这些任务有条不紊地在主线程上执行,页面进程引入了消息队列和事件循环机制,渲染进程内部会维护多个消息队列,比如延迟执行队列和普通的消息队列。然后主线程采用一个 for 循环,不断地从这些任务队列中取出任务并执行任务。我们把这些消息队列中的任务称为宏任务。
消息队列中的任务是通过事件循环系统来执行的,这里我们可以看看在WHATWG 规范中是怎么定义事件循环机制的。
由于规范需要支持语义上的完备性,所以通常写得都会比较啰嗦,这里我就大致总结了下 WHATWG 规范定义的大致流程:
- 先从多个消息队列中选出一个最老的任务,这个任务称为 oldestTask;
- 然后循环系统记录任务开始执行的时间,并把这个 oldestTask 设置为当前正在执行的任务;
- 当任务执行完成之后,删除当前正在执行的任务,并从对应的消息队列中删除掉这个 oldestTask;
- 最后统计执行完成的时长等信息。
以上就是消息队列中宏任务的执行过程,通过前面的学习,相信你也很熟悉这套执行流程了。
宏任务可以满足我们大部分的日常需求,不过如果有对时间精度要求较高的需求,宏任务就难以胜任了,下面我们就来分析下为什么宏任务难以满足对时间精度要求较高的任务。
前面我们说过,页面的渲染事件、各种 IO 的完成事件、执行 JavaScript 脚本的事件、用户交互的事件等都随时有可能被添加到消息队列中,而且添加事件是由系统操作的,JavaScript 代码不能准确掌控任务要添加到队列中的位置,控制不了任务在消息队列中的位置,所以很难控制开始执行任务的时间。为了直观理解,你可以看下面这段代码:
<!DOCTYPE html>
<html>
<body>
<div id='demo'>
<ol>
<li>test</li>
</ol>
</div>
</body>
<script type="text/javascript">
function timerCallback2(){
console.log(2)
}
function timerCallback(){
console.log(1)
setTimeout(timerCallback2,0)
}
setTimeout(timerCallback,0)
</script>
</html>
在这段代码中,我的目的是想通过 setTimeout 来设置两个回调任务,并让它们按照前后顺序来执行,中间也不要再插入其他的任务,因为如果这两个任务的中间插入了其他的任务,就很有可能会影响到第二个定时器的执行时间了。
但实际情况是我们不能控制的,比如在你调用 setTimeout 来设置回调任务的间隙,消息队列中就有可能被插入很多系统级的任务。你可以打开 Performance 工具,来记录下这段任务的执行过程,也可参考文中我记录的图片:

setTimeout 函数触发的回调函数都是宏任务,如图中,左右两个黄色块就是 setTimeout 触发的两个定时器任务。
现在你可以重点观察上图中间浅红色区域,这里有很多一段一段的任务,这些是被渲染引擎插在两个定时器任务中间的任务。试想一下,如果中间被插入的任务执行时间过久的话,那么就会影响到后面任务的执行了。
所以说宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合了,比如后面要介绍的监听 DOM 变化的需求。
面试官追问
追问 1线上监控显示设为 0 毫秒的 setTimeout 偶尔晚了几十毫秒,值班同事准备把它判定为定时器失效,你会接受这个结论吗?
不能据此判定定时器失效,回调只是作为宏任务等待事件循环选取,并不承诺零毫秒后立即执行。渲染、交互、脚本或 I/O 完成任务都可能先占用主线程;需要结合性能时间线确认排队和执行情况。
追问 2商品页希望两个计算步骤紧挨着执行,同事把第二步放进第一步的 setTimeout 回调中;这种写法能保证中间没有其他任务吗?
嵌套只能保证第二个定时器在第一个回调中被安排,不能保证两个回调之间没有系统任务。两个回调都是独立宏任务,渲染引擎可能在其间插入工作;若必须连续完成,应重新评估是否需要拆成两个定时任务。
追问 3仪表盘从每秒更新一次变成跟随拖拽实时更新,原来基于多个宏任务的处理方案还能满足时间精度要求吗?
不能直接假定原方案仍然合适,宏任务在多个消息队列中的位置由系统调度,开始时间难以精确掌控。拖拽频率提高后,排队和插入任务会放大延迟;应缩减单次工作,并判断其中是否有适合更细粒度调度的部分。
追问 4页面点击后动画明显卡顿,性能面板显示两个定时器之间穿插了布局、绘制和脚本任务,你会怎样解释第二个回调延迟?
第二个回调需要等待它之前被选中的宏任务依次完成,布局、绘制和脚本执行都会占用主线程。排查时应关注这些任务的持续时间及定时器实际入队位置;仅继续减小延迟参数无法消除消息队列中的竞争。
追问 5业务需要监听 DOM 是否变化,方案一是短间隔 setInterval,方案二是事件驱动通知;为什么宏任务轮询通常不是优先选择?
短间隔轮询会产生大量没有变化时的无效检查,长间隔又会降低通知及时性,而且宏任务的实际执行时间仍不可精确控制。针对 DOM 变化可优先考虑 MutationObserver;其回调虽更及时,仍需限制处理成本。
追问 6面试现场让你串起主线程、消息队列和事件循环:一次网络完成与一次页面绘制为什么可能影响定时器回调?
网络完成、绘制、交互和脚本执行都可能形成主线程需要处理的宏任务,并进入渲染进程维护的不同队列。事件循环会从队列中选择任务执行,JavaScript 无法精确指定定时器的位置;因此其他任务会改变其等待时间。
