DOM 规范中的 MutationObserver 接口
MutationObserver 接口
DOM规范中的 MutationObserver 接口可以在DOM被修改时异步执行回调。
使用MutationObserver可以观察整个文档、DOM树的一部分或某个元素,元素属性、字节点、文本等。
新引进的MutationObserver接口取代了已废弃的MutationEvent。
MutationObserver 实例
MutationObserver实例通过调用MutationObserver构造函数并传入一个回调函数创建。
新创建的MutationObserver实例,需要使用observe()方法与DOM进行关联。
observe()
两个必填参数:
要观察的DOM节点
MutationObserveInit 对象
MutationObserveInit对象用于控制观察哪些方面的变化,是一个键值对形式配置选项的字典。
回调与 MutationRecord
MutationObserver(构造函数)中的参数(回调函数),每个回调都会收到一个MutationRecord实例的数组(第一个参数),记录发生了什么变化以及DOM的哪部分收到了影响。
因为回调执行之前可能同时发生多个满足观察条件的事件,所以每次执行回调都会传入一个包含按顺序入队的MutationRecord实例的数组。
new MutationObserver(fn)
参数:回调函数fn。
回调函数fn的参数:
第一个参数:MutationRecord实例的数组
第二个参数:观察变化的MutationObserver的实例
MutationObserver
的回调函数可以多次执行。每当观察到 DOM 树的变化时,回调函数就会被调用。这些变化包括但不限于节点的添加、删除、属性的更改等。
那么问题来了
本例中,为什么回调中的 console.log
只输出了一次?
let ob = new MutationObserver(function () {console.log("%cbody attributes changed", "color: red; font-weight:bold;font-size: 20px;");
});
ob.observe(document.body, { attributes: true });
document.body.setAttribute("foo", "bar");
document.body.className = "newClass";
解答(来自通义灵码)
1.MutationObserver 是异步执行的
MutationObserver
的回调是通过 微任务队列(microtask queue) 异步触发的。- 当你在短时间内连续修改 DOM 属性时,浏览器会将这些变更合并为一个批次,并只调用一次回调函数。
2. 属性变更被合并处理
- 在你的代码中:
这两个操作都是对document.body.setAttribute("foo", "bar"); document.body.className = "newClass";
document.body
的属性进行修改。 - 因为它们在同一个事件循环中发生,所以
MutationObserver
只会收到一次“属性变化”的通知,并触发一次回调。
🧪 实验验证
如果你希望看到两次日志输出,可以使用 setTimeout
将这两个修改分开到不同的事件循环中:
document.body.setAttribute("foo", "bar"); setTimeout(() => { document.body.className = "newClass"; }, 0);
这样,MutationObserver
会在两个独立的 microtask 中检测到两次属性变更,从而触发两次回调。
✅ 总结
现象 | 原因 |
---|---|
console.log 只输出一次 | 浏览器将多个 DOM 修改合并为一次变更通知,回调只执行一次 |
关键点 | 说明 |
---|---|
异步机制 | MutationObserver 使用微任务队列处理回调 |
批量优化 | 浏览器自动合并多次变更以提升性能 |
控制输出次数 | 可通过 setTimeout 或其他方式分隔变更事件 |
🛠️ 建议改进代码
// 分开两次 DOM 操作,确保触发两次回调 document.body.setAttribute("foo", "bar"); setTimeout(() => { document.body.className = "newClass"; }, 0);
这样就能在控制台看到两次 "body attributes changed"
的日志输出。
再记录一个通用示例
部分代码来自夸克浏览器。
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>木木的学习笔记</title><style>#target {border: 1px solid black;padding: 10px;margin-top: 10px;}</style></head><body><div class="container"><div class="navBox"></div><p>MutationObserver 的回调函数可以多次执行。每当观察到 DOM 树的变化时,回调函数就会被调用。这些变化包括但不限于节点的添加、删除、属性的更改等。</p><p>测试</p><div id="target">这是一个目标元素。</div><br /><button onclick="changeText()">改变文本</button></div><script src="./js/nav.js"></script><script>//加载页面导航var navObj = {title: "MutationObserver",chapter: "",section: "",part: "",};setNav(navObj);</script><script>// 选择需要观察变动的节点const targetNode = document.getElementById("target");// 配置观察选项const config = { childList: true, subtree: true, attributes: true, characterData: true };// 创建一个回调函数,当观察到DOM变动时会被调用const callback = function (mutationsList, observer) {console.log("DOM changed");for (let mutation of mutationsList) {if (mutation.type === "childList") {console.log("A child node has been added or removed.");} else if (mutation.type === "attributes") {console.log(`The ${mutation.attributeName} attribute was modified.`);} else if (mutation.type === "characterData") {console.log("Character data in the node has been changed.");}}};// 创建一个观察器实例并传入回调函数const observer = new MutationObserver(callback);// 开始观察目标节点observer.observe(targetNode, config);// 函数用于改变目标元素的文本内容let count = 0;function changeText() {targetNode.textContent = `文本已被第 ${++count} 次改变!`;}</script></body>
</html>
这个示例中,每次修改都被记录。