
MutationObserver 的变动监听队列不是手动维护的“队列”结构,而是由浏览器引擎在 DOM 变更触发时,将变更记录(MutationRecord)批量暂存,并在当前宏任务结束、下一个微任务阶段统一派发回调。它的核心机制是“微任务调度 + 批量聚合”,而非传统 FIFO 队列。
变动记录如何被收集与暂存
当 JS 同步代码中执行如 el.appendChild()、node.setAttribute() 等操作时,浏览器会立即检测并生成对应的 MutationRecord 对象,但不立刻执行回调。这些记录被追加到一个内部待处理列表(per-observer 或全局 pending list),按触发顺序暂存,但尚未排序或去重。
- 同一轮 JS 执行中多次修改同一节点的属性,可能生成多条
attributes类型记录; - 若配置了
subtree: true,子树内任意层级的变更都会被收进该 observer 的待处理列表; - 不同 observer 实例彼此隔离,各自维护独立的待处理记录集合。
何时派发?为什么是微任务
待处理列表只在当前调用栈清空、且没有其他微任务正在执行时,才由浏览器自动清空并调用 observer 回调。这个时机等同于 Promise.resolve().then() 的执行时刻。
- 它与
Promise、queueMicrotask共享同一微任务队列,优先级高于setTimeout; - 即使连续触发 20 次
appendChild,也只派发一次回调,参数mutationsList是包含全部 20 条记录的数组; - 这种设计天然避免了高频变更导致的回调风暴,也保证了 UI 渲染(paint)不会被监听逻辑阻塞。
批量聚合的边界与限制
“批量”仅作用于同一轮事件循环的同步执行段。一旦中间插入异步分界(如 await、setTimeout(fn, 0)、用户交互事件),后续变更就会归入下一轮微任务。
-
注意:跨帧操作(如
requestAnimationFrame内修改 DOM)会落在不同宏任务中,必然触发两次独立回调; - 无法强制合并跨任务的变更,这是浏览器实现决定的,开发者不可干预;
- 若需自定义聚合(如 100ms 内所有变更合并处理),需在外层用
setTimeout或debounce封装回调逻辑。
记录内容不可变,但可被过滤与忽略
每个 MutationRecord 是只读快照,包含 type、target、addedNodes、removedNodes、attributeName、oldValue(需显式开启 attributeOldValue)等字段。浏览器不提供“取消某条记录”或“修改已生成记录”的能力。
- 所谓“精准监听”,靠的是配置项(如
attributeFilter: ['data-id'])在记录生成前就过滤掉无关变更; - 回调内仍需做运行时判断(例如检查
mutation.target.dataset.componentId),因为subtree: true可能捕获到框架内部临时节点; - 重复初始化问题必须由业务代码用
Set缓存 ID 或标记data-initialized属性来解决,浏览器不代劳。











