微任务主要由promise回调、mutationobserver和queuemicrotask产生。1.promise的.then()、.catch()、.finally()会在状态变化后将回调放入微任务队列;2.mutationobserver用于监听dom变化,其回调作为微任务批量处理以优化性能;3.queuemicrotask是es2021新增api,允许开发者显式安排微任务。这些微任务会在当前宏任务执行完毕后立即全部执行,确保异步操作顺序可控并优化ui更新,从而提升代码执行的一致性和性能表现。
JavaScript事件循环中,会产生微任务的操作主要集中在几个特定场景:Promise的回调函数(包括
.then()
,
.catch()
,
.finally()
),DOM的
MutationObserver
回调,以及通过
queueMicrotask()
API显式安排的任务。这些微任务会在当前宏任务(比如一次脚本执行、一个定时器回调)执行完毕后,但在下一个宏任务开始之前,被一并处理掉。
要深入理解微任务,我们得先把它放到JavaScript事件循环的大背景里看。我个人觉得,理解这个机制,是掌握JS异步编程的关键一步,否则很多时候你会发现代码执行顺序和你想的不一样,尤其是在处理UI更新或者复杂的异步流时。
简单来说,事件循环就像个永动机,它不停地检查两类任务队列:宏任务队列(Macrotask Queue)和微任务队列(Microtask Queue)。当我们执行一段JS代码时,它本身就是一个宏任务。这段宏任务执行过程中,如果遇到Promise的
.then()
回调,或者DOM发生了变化触发了
MutationObserver
,这些回调并不会立刻执行。它们会被悄悄地扔进一个叫“微任务队列”的地方。
立即学习“Java免费学习笔记(深入)”;
一个宏任务执行完了,事件循环并不会马上跳到下一个宏任务。它会先去看看微任务队列里有没有东西。如果有,它就会把队列里所有的微任务,一个接一个地、不间断地执行完,直到队列清空。这个过程是“原子性”的,意思是微任务队列一旦开始清空,就不会被打断,直到所有微任务都执行完毕,才会进行浏览器渲染(如果是在浏览器环境)或者去取下一个宏任务。
所以,核心逻辑就是:一个宏任务 -> 清空所有微任务 -> 渲染(浏览器特有) -> 下一个宏任务。理解这个顺序,你就能明白为什么
Promise.resolve().then(...)
会比
setTimeout(..., 0)
先执行了。
Promise回调为什么是微任务,它和异步有什么关系?
Promise回调被设计成微任务,这背后其实有很深层的考量。你想啊,Promise是为了解决回调地狱,让异步代码更易读、更可控。如果它的回调(比如
then
里头的函数)是宏任务,那会发生什么?
举个例子,假设你有一个Promise链,每一个
then
都对应一个宏任务。那么,当你执行完第一个
then
后,事件循环可能就会去处理其他的宏任务,比如一个
setTimeout
,或者甚至进行一次页面渲染,然后才回来执行你的下一个
then
。这会导致什么问题?
- 执行顺序不确定性增加: 你的Promise链会被其他宏任务“打断”,导致链式调用的逻辑变得难以预测。
- UI更新的抖动: 如果Promise的某个步骤需要更新UI,而它又是宏任务,那么UI可能会在Promise链的中间步骤被多次更新,造成不必要的重绘和性能损耗,甚至出现闪烁。
把Promise回调设计成微任务,就完美地解决了这些问题。当一个Promise状态从pending变为fulfilled或rejected时,其对应的
then
/
catch
/
finally
回调会被立即放入微任务队列。这意味着:
- 紧密耦合: 这些回调会紧跟着当前正在执行的同步代码和所有其他微任务之后执行,确保了Promise链的连续性和“立即性”,它们不会被其他宏任务插队。
- UI更新优化: 在一个宏任务内部,所有由它触发的微任务都会在渲染之前执行。这样,即使你的Promise链条很长,所有相关的DOM操作和数据更新都可以在同一次渲染前完成,避免了多次不必要的重绘,提供了更流畅的用户体验。
我个人觉得,这是Promise设计上最精妙的地方之一,它在保证异步性的同时,又通过微任务机制提供了接近同步的执行确定性。
除了Promise,还有哪些常见的操作会产生微任务,它们有什么用?
除了Promise,微任务的家族成员还有几个。最常见的,我觉得是
MutationObserver
和相对较新的
queueMicrotask
。
1. MutationObserver
这玩意儿听名字就知道跟“变动观察”有关,它就是用来监听DOM树变化的。比如说,你想知道页面上某个元素的属性变了没有,或者子节点被添加删除了没有,
MutationObserver
就能派上用场。
它的回调函数,也就是当DOM变化被观察到时执行的那个函数,就是作为微任务被安排的。为什么是微任务?因为DOM变化可能非常频繁,而且很多时候,一个小的DOM操作可能会连锁引发一系列变化。如果
MutationObserver
的回调是宏任务,那每次DOM变动都可能触发一次宏任务,造成大量的上下文切换和不必要的UI重绘。
作为微任务,
MutationObserver
的回调就能在当前宏任务结束时,批量处理所有观察到的DOM变化。这意味着,即使你在一个宏任务里对DOM进行了十几次修改,
MutationObserver
的回调也只会执行一次(或者说,它会收集所有变化,然后一次性通知你),而且是在浏览器渲染之前。这大大提升了性能,避免了UI的抖动和不必要的计算。
2. queueMicrotask()
这个API是ES2021引入的,它的目的非常明确:允许开发者直接、显式地将一个函数安排为微任务。
console.log('同步代码开始'); queueMicrotask(() => { console.log('这是一个微任务'); }); Promise.resolve().then(() => { console.log('这是另一个微任务 (Promise)'); }); console.log('同步代码结束'); // 实际输出顺序: // 同步代码开始 // 同步代码结束 // 这是一个微任务 // 这是一个另一个微任务 (Promise)
你可能会问,有了Promise,有了
setTimeout(0)
,为什么还需要
queueMicrotask
?它的价值在于,它提供了一个比
setTimeout(0)
更“即时”的异步执行机制。
setTimeout(0)
实际上是把任务放到了宏任务队列的末尾,它会等待当前所有微任务和可能的渲染完成后才执行。而
queueMicrotask
则保证了你的回调会在当前宏任务结束后,所有其他微任务执行之前或之中(取决于入队顺序)执行。
我个人觉得,
queueMicrotask
在某些高级场景下非常有用,比如当你需要确保某个操作在UI更新前完成,但又不想引入Promise的额外开销时。它给了开发者更细粒度的控制权,让你能更精确地控制异步代码的执行时机。
微任务与宏任务的执行顺序和区别,对实际开发有什么影响?
理解微任务和宏任务的根本区别以及它们的执行顺序,我觉得是深入前端优化的一个必经之路。说白了,它们决定了你的异步代码什么时候真正跑起来。
核心区别:
- 宏任务 (Macrotasks): 可以理解为一次“完整”的事件循环周期。