Mutation Observer API(Mutation Observer API)
一、MutationObserver 的背景
1.1 从 MutationEvent 到 MutationObserver
在 MutationObserver 出现之前,DOM 变化是通过 MutationEvent API 来监听的。但 MutationEvent 有严重的设计缺陷:它是同步的。这意味着每次 DOM 变化都会立即触发事件,在大量 DOM 操作时会导致严重的性能问题。
// 已废弃的 MutationEvent
element.addEventListener('DOMNodeInserted', (event) => {
console.log('Node inserted:', event.target);
});
MutationEvent 的问题包括:
- 同步执行,性能差
- 不支持观察子树的所有变化
- API 设计不合理,很多事件类型从未被正确实现
1.2 MutationObserver 的设计
MutationObserver 是异步观察者模式的应用:
- 异步执行:DOM 变化不会立即触发回调,而是被批量收集后在微任务队列中执行
- 按需观察:可以精确指定要观察的变化类型
- 低开销:浏览器内部优化,不需要重复扫描整个 DOM 树
// 基本的 MutationObserver 使用
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
console.log('Type:', mutation.type);
console.log('Target:', mutation.target);
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
二、API 详解
2.1 创建观察者
const observer = new MutationObserver(callback);
// callback 接收 MutationRecord 数组
function callback(mutations, observer) {
mutations.forEach(mutation => {
// 处理每个变化
});
}
2.2 observe 方法
observer.observe(target, options);
// options 定义要观察的变化类型
const options = {
// 观察子节点的添加/删除
childList: false,
// 观察后代节点的变化
subtree: false,
// 观察属性的添加/删除/修改
attributes: false,
// 观察属性值的变化(需要 attributes: true)
attributeOldValue: false,
// 观察文本内容的变化
characterData: false,
// 观察文本节点的旧值(需要 characterData: true)
characterDataOldValue: false,
// 只观察指定属性
attributeFilter: ['class', 'data-*']
};
2.3 MutationRecord 类型
mutations.forEach(mutation => {
switch (mutation.type) {
case 'childList':
// 子节点的添加、删除、重排序
console.log('Added nodes:', mutation.addedNodes);
console.log('Removed nodes:', mutation.removedNodes);
console.log('Previous sibling:', mutation.previousSibling);
console.log('Next sibling:', mutation.nextSibling);
break;
case 'attributes':
// 属性变化
console.log('Attribute name:', mutation.attributeName);
console.log('Old value:', mutation.oldValue);
console.log('New value:', mutation.target.getAttribute(mutation.attributeName));
break;
case 'characterData':
// 文本节点内容变化
console.log('Old value:', mutation.oldValue);
console.log('New value:', mutation.target.textContent);
break;
}
});
三、实际应用场景
3.1 动态内容检测
// 检测页面动态加载的内容
class DynamicContentDetector {
constructor() {
this.observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
this.onElementAdded(node);
}
});
});
});
}
start() {
this.observer.observe(document.body, {
childList: true,
subtree: true
});
}
stop() {
this.observer.disconnect();
}
onElementAdded(element) {
// 检测脚本标签
if (element.tagName === 'SCRIPT') {
console.log('Script added:', element.src || 'inline');
}
// 检测图片懒加载
if (element.tagName === 'IMG' && element.dataset.src) {
console.log('Lazy image detected');
}
// 检测第三方嵌入
if (element.tagName === 'IFRAME') {
console.log('iframe embedded:', element.src);
}
}
}
3.2 表单变化追踪
// 追踪表单字段的变化
class FormChangeTracker {
constructor(form) {
this.form = form;
this.originalValues = new Map();
this.changedFields = new Set();
this.observer = null;
this.init();
}
init() {
// 记录初始值
this.form.querySelectorAll('input, select, textarea').forEach(field => {
this.originalValues.set(field, this.getFieldValue(field));
});
// 观察变化
this.observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
this.trackNewFields(node);
}
});
mutation.removedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
this.untrackFields(node);
}
});
}
});
});
this.observer.observe(this.form, {
childList: true,
subtree: true
});
// 监听 input 事件
this.form.addEventListener('input', (e) => {
this.onFieldChange(e.target);
});
}
getFieldValue(field) {
if (field.type === 'checkbox' || field.type === 'radio') {
return field.checked;
}
return field.value;
}
trackNewFields(root) {
root.querySelectorAll('input, select, textarea').forEach(field => {
if (!this.originalValues.has(field)) {
this.originalValues.set(field, this.getFieldValue(field));
}
});
}
untrackFields(root) {
root.querySelectorAll('input, select, textarea').forEach(field => {
this.originalValues.delete(field);
this.changedFields.delete(field);
});
}
onFieldChange(field) {
const original = this.originalValues.get(field);
const current = this.getFieldValue(field);
if (original !== current) {
this.changedFields.add(field);
} else {
this.changedFields.delete(field);
}
}
hasUnsavedChanges() {
return this.changedFields.size > 0;
}
getChangedFields() {
return Array.from(this.changedFields).map(field => ({
name: field.name,
original: this.originalValues.get(field),
current: this.getFieldValue(field)
}));
}
destroy() {
this.observer.disconnect();
}
}
3.3 实现简易响应式数据绑定
// 基于 MutationObserver 的简易数据绑定
class ReactiveBinder {
constructor() {
this.bindings = new Map();
this.observer = new MutationObserver((mutations) => {
this.handleMutations(mutations);
});
}
// 绑定数据源到元素
bind(source, expression, element, property = 'textContent') {
if (!this.bindings.has(source)) {
this.bindings.set(source, new Set());
}
this.bindings.get(source).add({ expression, element, property });
this.updateBinding(source, expression, element, property);
}
// 开始观察
observe(root) {
this.observer.observe(root, {
childList: true,
subtree: true,
characterData: true
});
}
handleMutations(mutations) {
mutations.forEach(mutation => {
if (mutation.type === 'characterData') {
// 文本变化,触发更新
const source = mutation.target;
const bindings = this.bindings.get(source);
if (bindings) {
bindings.forEach(binding => {
this.updateBinding(source, binding.expression, binding.element, binding.property);
});
}
}
});
}
updateBinding(source, expression, element, property) {
try {
// 简单表达式解析
const value = expression(source);
element[property] = value;
} catch (e) {
// 忽略绑定错误
}
}
disconnect() {
this.observer.disconnect();
}
}
四、与框架的集成
4.1 虚拟 DOM diffing 辅助
// 使用 MutationObserver 辅助虚拟 DOM 对比
class VDOMObserver {
constructor(root, render) {
this.root = root;
this.render = render;
this.observer = null;
this.pending = false;
this.init();
}
init() {
this.observer = new MutationObserver(() => {
if (!this.pending) {
this.pending = true;
requestAnimationFrame(() => {
this.reconcile();
this.pending = false;
});
}
});
this.observer.observe(this.root, {
childList: true,
subtree: true,
attributes: true,
characterData: true
});
}
reconcile() {
// 触发重新渲染
// 实际框架会进行精细的 diff
this.render();
}
disconnect() {
this.observer.disconnect();
}
}
4.2 自动化测试中的 DOM 监控
// 测试工具:等待特定 DOM 变化
async function waitForElement(selector, timeout = 5000) {
const element = document.querySelector(selector);
if (element) return element;
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
observer.disconnect();
reject(new Error(`Element ${selector} not found within ${timeout}ms`));
}, timeout);
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches && node.matches(selector)) {
clearTimeout(timeoutId);
observer.disconnect();
resolve(node);
}
const found = node.querySelector(selector);
if (found) {
clearTimeout(timeoutId);
observer.disconnect();
resolve(found);
}
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
五、性能考虑
5.1 观察范围最小化
// ❌ 过度观察
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
characterData: true
});
// ✅ 精确观察
observer.observe(specificElement, {
childList: true, // 只关心子节点变化
// 不需要 subtree, attributes, characterData
});
5.2 使用 attributeFilter
// ❌ 观察所有属性变化
observer.observe(element, {
attributes: true
});
// ✅ 只观察特定属性
observer.observe(element, {
attributes: true,
attributeFilter: ['class', 'data-active', 'disabled']
});
5.3 批量处理与防抖
// 批量处理变化
const observer = new MutationObserver((mutations) => {
// 在一次重排中处理所有变化
const added = [];
const removed = [];
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
added.push(...mutation.addedNodes);
removed.push(...mutation.removedNodes);
}
});
// 统一更新
updateUI(added, removed);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
六、disconnect 和 takeRecords
6.1 disconnect
// 停止观察
observer.disconnect();
// 清理时必须调用
observer.disconnect();
6.2 takeRecords
// 获取未处理的记录(调用 disconnect 前)
const pending = observer.takeRecords();
console.log('Pending mutations:', pending.length);
pending.forEach(mutation => {
// 处理未触发回调的变化
});