pin_drop当前位置:知识文库 ❯ 图文
HTML5 API:HTML5 Mutation Observer - 完整教程与代码示例
一、教程简介
Mutation Observer API 提供了监视 DOM 树变化的能力,可以监听元素的属性变更、子节点增删、文本内容修改等 DOM 变动。相比已废弃的 Mutation Events,Mutation Observer 采用异步批量处理机制,性能更优,不会导致页面卡顿。该 API 广泛应用于数据绑定框架、富文本编辑器、无限滚动、DOM 变化追踪等场景。
二、核心概念
与 Mutation Events 对比
监听选项
MutationRecord
每次变化产生的记录对象:
三、语法与用法
创建与连接
代码示例
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log('变化类型:', mutation.type);
console.log('目标元素:', mutation.target);
});
});
observer.observe(targetElement, {
childList: true,
attributes: true,
characterData: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});断开与恢复
代码示例
// 停止监听
observer.disconnect();
// 获取未处理的记录
const records = observer.takeRecords();四、代码示例
示例一:DOM 变化监控面板
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mutation Observer - DOM 监控</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #1a1a2e;
min-height: 100vh;
padding: 30px 20px;
color: #e0e0e0;
}
.container { max-width: 800px; margin: 0 auto; }
h1 { font-size: 24px; color: #fff; margin-bottom: 8px; }
.desc { color: #7a8ba0; font-size: 14px; margin-bottom: 24px; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.card {
background: #16213e;
border-radius: 12px;
padding: 24px;
border: 1px solid #1a3a5c;
}
.card h2 { font-size: 16px; color: #fff; margin-bottom: 16px; }
.target-area {
background: #0f3460;
border-radius: 8px;
padding: 16px;
min-height: 150px;
margin-bottom: 12px;
}
.target-item {
padding: 8px 12px;
background: #1a3a5c;
border-radius: 6px;
margin-bottom: 6px;
font-size: 13px;
display: flex;
justify-content: space-between;
align-items: center;
}
.target-item .remove-btn {
color: #e17055;
cursor: pointer;
font-weight: 700;
}
.btn-group { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
button {
padding: 6px 14px;
border: none;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.btn-cyan { background: #00cec9; color: #1a1a2e; }
.btn-cyan:hover { background: #00b5ad; }
.btn-orange { background: #fdcb6e; color: #1a1a2e; }
.btn-orange:hover { background: #e5b85c; }
.btn-red { background: #e17055; color: #fff; }
.btn-red:hover { background: #d35f48; }
.btn-purple { background: #a29bfe; color: #1a1a2e; }
.btn-purple:hover { background: #8c82f7; }
.log-area {
background: #0a0a1a;
border-radius: 8px;
padding: 12px;
max-height: 350px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
line-height: 1.7;
}
.log-entry { padding: 2px 0; }
.log-entry.childList { color: #00cec9; }
.log-entry.attributes { color: #fdcb6e; }
.log-entry.characterData { color: #a29bfe; }
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 12px;
}
.stat {
background: #0f3460;
border-radius: 6px;
padding: 10px;
text-align: center;
}
.stat-label { font-size: 10px; color: #7a8ba0; }
.stat-value { font-size: 16px; font-weight: 700; color: #00cec9; }
input[type="text"] {
width: 100%;
padding: 6px 10px;
border: 1px solid #1a3a5c;
border-radius: 6px;
background: #0f3460;
color: #e0e0e0;
font-size: 13px;
outline: none;
margin-bottom: 8px;
}
input:focus { border-color: #00cec9; }
.observing { border: 2px solid #00cec9 !important; }
</style>
</head>
<body>
<div class="container">
<h1>Mutation Observer 监控</h1>
<p class="desc">对目标区域进行 DOM 操作,观察 Mutation Observer 捕获的变化记录。</p>
<div class="stats">
<div class="stat">
<div class="stat-label">childList 变化</div>
<div class="stat-value" id="childListCount">0</div>
</div>
<div class="stat">
<div class="stat-label">attributes 变化</div>
<div class="stat-value" id="attrCount">0</div>
</div>
<div class="stat">
<div class="stat-label">characterData 变化</div>
<div class="stat-value" id="charDataCount">0</div>
</div>
</div>
<div class="grid">
<div>
<div class="card">
<h2>目标区域</h2>
<div class="target-area" id="targetArea">
<div class="target-item" data-id="1">项目 1 <span class="remove-btn" onclick="removeItem(this)">×</span></div>
<div class="target-item" data-id="2">项目 2 <span class="remove-btn" onclick="removeItem(this)">×</span></div>
<div class="target-item" data-id="3">项目 3 <span class="remove-btn" onclick="removeItem(this)">×</span></div>
</div>
<input type="text" id="newItemText" placeholder="输入新项目文本">
<div class="btn-group">
<button class="btn-cyan" onclick="addItem()">添加节点</button>
<button class="btn-orange" onclick="changeAttribute()">修改属性</button>
<button class="btn-purple" onclick="changeText()">修改文本</button>
<button class="btn-red" onclick="clearTarget()">清空</button>
</div>
<div class="btn-group">
<button class="btn-cyan" id="toggleBtn" onclick="toggleObserver()">开始监听</button>
</div>
</div>
</div>
<div>
<div class="card">
<h2>变化记录</h2>
<div class="log-area" id="logArea">
<div class="log-entry" style="color:#7a8ba0;">等待 DOM 变化...</div>
</div>
</div>
</div>
</div>
</div>
<script>
let observer = null;
let isObserving = false;
let counts = { childList: 0, attributes: 0, characterData: 0 };
function toggleObserver() {
const btn = document.getElementById('toggleBtn');
const target = document.getElementById('targetArea');
if (isObserving) {
observer.disconnect();
isObserving = false;
btn.textContent = '开始监听';
target.classList.remove('observing');
} else {
observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
counts[mutation.type]++;
document.getElementById('childListCount').textContent = counts.childList;
document.getElementById('attrCount').textContent = counts.attributes;
document.getElementById('charDataCount').textContent = counts.characterData;
logMutation(mutation);
});
});
observer.observe(target, {
childList: true,
attributes: true,
characterData: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});
isObserving = true;
btn.textContent = '停止监听';
target.classList.add('observing');
}
}
function logMutation(m) {
const log = document.getElementById('logArea');
if (counts.childList + counts.attributes + counts.characterData === 1) {
log.innerHTML = '';
}
const time = new Date().toLocaleTimeString('zh-CN');
let msg = '[' + time + '] ';
if (m.type === 'childList') {
if (m.addedNodes.length > 0) {
msg += '添加节点: ' + Array.from(m.addedNodes).map(n => n.textContent?.trim() || n.nodeName).join(', ');
}
if (m.removedNodes.length > 0) {
msg += '删除节点: ' + Array.from(m.removedNodes).map(n => n.textContent?.trim() || n.nodeName).join(', ');
}
} else if (m.type === 'attributes') {
msg += '属性变化: ' + m.attributeName + ' (' + (m.oldValue || 'null') + ' → ' + m.target.getAttribute(m.attributeName) + ')';
} else if (m.type === 'characterData') {
msg += '文本变化: (' + (m.oldValue || '') + ' → ' + m.target.textContent + ')';
}
const entry = document.createElement('div');
entry.className = 'log-entry ' + m.type;
entry.textContent = msg;
log.insertBefore(entry, log.firstChild);
}
let itemId = 4;
function addItem() {
const text = document.getElementById('newItemText').value.trim() || '项目 ' + itemId;
const target = document.getElementById('targetArea');
const item = document.createElement('div');
item.className = 'target-item';
item.dataset.id = itemId++;
item.innerHTML = text + ' <span class="remove-btn" onclick="removeItem(this)">×</span>';
target.appendChild(item);
document.getElementById('newItemText').value = '';
}
function removeItem(btn) { btn.parentElement.remove(); }
function changeAttribute() {
const items = document.querySelectorAll('.target-item');
if (items.length === 0) return;
const item = items[Math.floor(Math.random() * items.length)];
const attrs = ['data-highlight', 'data-status', 'data-priority'];
const attr = attrs[Math.floor(Math.random() * attrs.length)];
const values = ['active', 'pending', 'done', 'important'];
item.setAttribute(attr, values[Math.floor(Math.random() * values.length)]);
}
function changeText() {
const items = document.querySelectorAll('.target-item');
if (items.length === 0) return;
const item = items[Math.floor(Math.random() * items.length)];
const texts = ['已更新', '修改后的内容', '新文本', 'changed'];
const removeBtn = item.querySelector('.remove-btn');
item.firstChild.textContent = texts[Math.floor(Math.random() * texts.length)] + ' ';
}
function clearTarget() { document.getElementById('targetArea').innerHTML = ''; }
</script>
</body>
</html>五、浏览器兼容性
六、注意事项与最佳实践
1. 避免无限循环
代码示例
// 修改 DOM 可能触发新的 mutation,导致无限循环
const observer = new MutationObserver(function(mutations) {
// 不要在回调中修改被观察的 DOM
// 如果必须修改,先 disconnect 再修改再重新 observe
observer.disconnect();
// 修改 DOM...
observer.observe(target, config);
});2. 性能优化
代码示例
// 只监听需要的类型
observer.observe(target, {
childList: true, // 只监听子节点变化
// 不设置 attributes 和 characterData
});
// 使用 attributeFilter 限制属性监听范围
observer.observe(target, {
attributes: true,
attributeFilter: ['class', 'data-status'] // 只监听这两个属性
});3. 及时断开
代码示例
// 不再需要时断开观察
observer.disconnect();
// 单次观察后自动断开
const observer = new MutationObserver(function(mutations) {
console.log('检测到变化:', mutations);
observer.disconnect();
});七、代码规范示例
代码示例
class DOMWatcher {
constructor(target, options = {}) {
this.target = target;
this.options = {
childList: true,
attributes: true,
characterData: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true,
attributeFilter: options.attributeFilter,
...options
};
this.callbacks = { childList: [], attributes: [], characterData: [] };
this.observer = null;
}
on(type, callback) {
if (this.callbacks[type]) {
this.callbacks[type].push(callback);
}
return this;
}
start() {
this.observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
this.callbacks[mutation.type]?.forEach(cb => cb(mutation));
});
});
this.observer.observe(this.target, this.options);
return this;
}
stop() {
this.observer?.disconnect();
return this;
}
getPendingRecords() {
return this.observer?.takeRecords() || [];
}
}
// 使用示例
const watcher = new DOMWatcher(document.getElementById('container'), {
attributeFilter: ['class', 'data-state']
});
watcher
.on('childList', (m) => console.log('子节点变化:', m))
.on('attributes', (m) => console.log('属性变化:', m.attributeName))
.start();八、常见问题与解决方案
常见问题
Mutation Observer 回调中修改 DOM 导致循环?
在回调中先 disconnect(),修改完 DOM 后再 observe()。
如何只监听直接子节点变化?
不设置 subtree: true,只监听直接子节点。
大量 DOM 变化时性能问题?
使用 attributeFilter 限制范围,避免 subtree: true 监听大型 DOM 树,考虑使用防抖处理。
九、总结
Mutation Observer 是监听 DOM 变化的推荐方式,采用异步批量处理机制,性能优于已废弃的 Mutation Events。通过配置监听选项可以精确控制监听范围,避免不必要的性能开销。使用时需注意避免在回调中修改被观察的 DOM(防止循环),及时断开不再需要的观察,以及合理使用 attributeFilter 和 subtree 选项优化性能。
标签:
DOM监听
MutationObserver
DOM变化
异步监听
HTML5 API
本文涉及AI创作
内容由AI创作,请仔细甄别