pin_drop当前位置:知识文库 ❯ 图文

HTML5 API:HTML5 Mutation Observer - 完整教程与代码示例

一、教程简介

Mutation Observer API 提供了监视 DOM 树变化的能力,可以监听元素的属性变更、子节点增删、文本内容修改等 DOM 变动。相比已废弃的 Mutation Events,Mutation Observer 采用异步批量处理机制,性能更优,不会导致页面卡顿。该 API 广泛应用于数据绑定框架、富文本编辑器、无限滚动、DOM 变化追踪等场景。


二、核心概念

与 Mutation Events 对比

特性 Mutation Events Mutation Observer
触发方式 同步 异步(微任务)
性能 频繁触发,可能卡顿 批量处理,性能优
状态 已废弃 推荐
阻止冒泡 可以 不适用
内存泄漏 容易(需手动移除) 不容易(断开即释放)

监听选项

选项 说明
childList 监听子节点的增删
attributes 监听属性变化
characterData 监听文本内容变化
subtree 监听所有后代节点
attributeOldValue 记录属性变化前的值
characterDataOldValue 记录文本变化前的值
attributeFilter 只监听指定的属性

MutationRecord

每次变化产生的记录对象:

属性 说明
type 变化类型:childList/attributes/characterData
target 发生变化的节点
addedNodes 新增的节点列表
removedNodes 删除的节点列表
previousSibling 前一个兄弟节点
nextSibling 后一个兄弟节点
attributeName 变化的属性名
attributeNamespace 属性命名空间
oldValue 变化前的值

三、语法与用法

创建与连接

代码示例

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)">&times;</span></div>
                        <div class="target-item" data-id="2">项目 2 <span class="remove-btn" onclick="removeItem(this)">&times;</span></div>
                        <div class="target-item" data-id="3">项目 3 <span class="remove-btn" onclick="removeItem(this)">&times;</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)">&times;</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>

五、浏览器兼容性

浏览器 支持版本
Chrome 26+
Firefox 14+
Safari 7+
Edge 12+
Opera 15+
IE 11+
iOS Safari 7+
Android 4.4+

六、注意事项与最佳实践

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(防止循环),及时断开不再需要的观察,以及合理使用 attributeFiltersubtree 选项优化性能。

标签: DOM监听 MutationObserver DOM变化 异步监听 HTML5 API

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: HTML5 API:HTML5 Clipboard API - 完整教程与代码示例 下一篇: HTML5 API:HTML5 Intersection Observ - 完整教程与代码示例

poll相关推荐