14.2 MutationObserver接口

MutationObserver 出现之前,开发者监听DOM变化的手段非常有限且粗糙,使用DOM Mutation Events (已废弃)DOMNodeInserted, DOMSubtreeModified 等。它们是事件,采用观察者模式,同步触发。这意味着在DOM变化的同一循环中,回调函数会立即执行。如果进行复杂的批量DOM操作,会连续触发大量事件,极易导致性能问题甚至浏览器崩溃。

MutationObserver 可以在DOM被修改时异步执行回调,可以观察整个文档、DOM树的一部分或某个元素。并且一次性批量处理所有记录,而不是每个变化触发一次(高效)。并且可以在回调中能获取到所有变更记录的详细列表,包括变更的类型、目标、旧值等。

在现代技术语境下MutationObserver 是构建响应式、可交互Web应用的基石,尤其是在开发第三方库、浏览器扩展、性能监控、富文本编辑器以及处理由现代前端框架(如 Vue、React)驱动的动态内容时,它不可或缺。

14.2.1基本用法

1. 创建观察者:new MutationObserver(callback)

构造函数接收一个回调函数作为参数。这个回调函数有两个参数:

  • mutations:一个 MutationRecord 对象的数组,包含了所有排队等待处理的变更记录。
  • observer:调用回调函数的 MutationObserver 实例本身。
const observer = new MutationObserver((mutations, observer) => {
  for (const mutation of mutations) {
    console.log('发生了变化:', mutation.type, mutation.target);
  }
});
2. observe(target, config)方法

接收两个必需参数:要观察其变化的DOM节点,以及一个MutationObserverInit对象。 MutationObserverInit对象是一个键/值对形式配置选项的字典,用于控制观察哪些方面的变化。该方法用于将新创建的MutationObserver实例关联到指定的DOM。

// 常用配置:监听元素自身及其后代的子节点增删、属性变化(但只过滤class和data-status)
const config = {
  childList: true,
  subtree: true,
  attributes: true,
  attributeFilter: ['class', 'data-status'] // 性能优化!
};
const observer = new MutationObserver(() => {
    console.log('发生了变化');
});
const targetNode = document.getElementById('app');
observer.observe(targetNode, config);
targetNode.className = 'foo';
console.log('Changed app class');
//Changed app class
//发生了变化
//以上结果表明验证了回调是与实际的DOM变化异步执行的。

3. 回调与MutationRecord

每次回调被触发时,你都会收到一个 MutationRecord 实例的数组。MutationRecord 实例包含的信息描述了一个独立的DOM变化。

下表为 MutationRecord 实例的属性。

属性

描述

type

变化的类型:'attributes'(属性),'childList'(子节点),'characterData'(文本数据)。

target

受变化影响的节点。对于属性变化,是属性所在的元素;对于子节点变化,是子节点的父节点。

addedNodes

返回新增的节点的 NodeList

removedNodes

返回被移除的节点的 NodeList

previousSibling

返回被添加或移除的节点的前一个兄弟节点,否则为 null

nextSibling

返回被添加或移除的节点的后一个兄弟节点,否则为 null

attributeName

返回被修改的属性的本地名称,否则为 null

attributeNamespace

返回被修改的属性的命名空间,否则为 null

oldValue

如果配置中对应选项为 trueattributeOldValue/characterDataOldValue

),则返回变化前的值。

const callback = (mutations) => {
  mutations.forEach(mutation => {
    if (mutation.type === 'childList') {
      console.log('有节点被添加:', mutation.addedNodes);
      console.log('有节点被移除:', mutation.removedNodes);
    } else if (mutation.type === 'attributes') {
      console.log(`属性 ${mutation.attributeName} 在 ${mutation.target} 上发生了变化`);
      console.log(`旧值是: ${mutation.oldValue}`);
    }
  });
};
4. disconnect()方法

默认情况下,只要被观察的元素不被垃圾回收,MutationObserver 的回调就会响应DOM变化事件,从而被执行。disconnect()可以提前终止回调。不仅会停止此后变化事件的回调,也会抛弃已经加入任务队列要异步执行的回调,但可以通过setTimeout()让已经入列的回调函数执行完毕后再终止:

const observer = new MutationObserver(() => {
    console.log('发生了变化');
});
const targetNode = document.getElementById('app');
observer.observe(targetNode, { attributes: true} );
targetNode.className = 'foo';
setTimeout(() => {
  observer.disconnect();
  //后续变化不触发回调函数
  targetNode.className = 'bar';
},0);
//发生了变化
5. 复用MutationObserver

多次调用observe()方法可以复用一个MutationObserver对象观察多个不同的目标节点。且因为disconnect()并不会结束MutationObserver的生命,所以也可以通过重新调用观察着并关联到新的节点目标恢复观察者与DOM元素的关联。

const observer = new MutationObserver((mutations, observer) => {
  for (const mutation of mutations) {
    console.log('发生了变化:', mutation.type, mutation.target);
  }
});
const divNode = document.getElementById('div'),
      spanNode = document.getElementById('span');
//观察两个子节点
observer.observe(divNode,{ attributes: true});
observer.observe(spanNode,{ attributes: true});
//这行代码会触发变化事件
divNode.setAttribute('foo','bar');
setTimeout(() => {
  observer.disconnect();
  //这行代码不触发变化事件
  spanNode.setAttribute('foo','bar');
},0);

setTimeout(() => {
  observer.observe(spanNode,{ attributes: true});
  //这行代码会触发变化事件
  spanNode.setAttribute('foo','bar');
},0);
6. MutationObserverInit与观察范围

MutationObserverInit对象用于控制对目标节点的观察范围。包括属性变化、文本变化和子节点变化。

MutationObserverInit对象属性表如下:

属性

描述

现代应用场景

subtree

布尔值。是否观察目标节点的所有后代节点(子树)。默认为 false

几乎总是设为 true,用于监听整个子树的变化,例如监听整个SPA的内容区域。

childList

布尔值。是否观察目标节点的子节点(增删)。默认为 false

核心选项。用于监听元素的直接子元素变化,如列表项的增删、组件的挂载/卸载。

attributes

布尔值。是否观察目标节点的属性变化。默认为 false

监听 class, style, data-*

等属性的变化。常用于响应式UI更新。

attributeFilter

字符串数组。指定要观察的属性名白名单,避免监听所有属性。

性能优化关键。例如 ['class', 'data-status'],只监听特定属性的变化。

attributeOldValue

布尔值。是否在 MutationRecord中记录变化前的属性值。默认为 false

需要知道属性从什么值变过来时使用。

characterData

布尔值。是否观察目标节点(文本节点)的文本内容变化。默认为 false

在开发富文本编辑器或需要监控文本变化时使用。

characterDataOldValue

布尔值。是否记录变化前的文本数据。

attributeOldValue,用于文本。

注意:在调用observe()时,MutationObserverInit对象属性中的attributescharacterDatachildList属性必须至少有一项为true,否则抛出错误。

重要配置示例:

// 常用配置:监听元素自身及其后代的子节点增删、属性变化(但只过滤class和data-status)
const config = {
  childList: true,	//观察子节点的添加和移除
  subtree: true,//默认情况下观察范围仅为一个元素及其子节点的变化,但是该属性设置为true可以扩展范围到该元素的子树(所有子节点)
  attributes: true,//默认观察所有属性,但是不会记录原来的属性值。
  attributeFilter: ['class', 'data-status'], // 性能优化!只会记录该白名单包含的属性变化
  attributeOldValue: true //在变化记录中保留属性原来的值。
};

14.2.2 异步回调与记录队列

MutationObserver 核心是异步回调与记录队列模型。为了性能不受大量事件变化影响,每次变化的信息都会保存在MutationRecord实例中,然后添加到记录队列中。每个队列对每个MutationObserver 实例都是唯一的,是所有DOM变化事件的有序列表。

takeRecords()方法

调用MutationObserver 实例的takeRecords()方法可以清空观察者的记录队列,并返回其中的所有 MutationRecord 对象。这个方法在你想要同步处理所有挂起的变更,而不等待回调触发时有用。

14.2.3 性能、内存与垃圾回收

1. MutationObserver 的引用

MutationObserver 实例与目标节点之间的引用关系是非对称的MutationObserver 拥有对要观察的目标节点的弱引用,不会妨碍垃圾回收程序回收目标节点。但是目标节点却拥有对MutationObserver强引用。如果目标节点从DOM中被移除并被垃圾回收,那么关联的MutationObserver 也会被垃圾回收。

2. MutationRecord 的引用

记录队列中的每个MutationRecord 实例至少包含对已有DOM节点的一个引用。如果变化是childList类型,则会包含多个节点的引用。记录队列和回调处理的默认行为就是耗尽这个队列,处理每个MutationRecord ,然后让它们超出作用域并被垃圾回收。

有时候需要保存整个观察者的完整变化记录。保存这些MutationRecord 实例,也就会保存它们的引用,因而妨碍这些节点被垃圾回收。如果需要尽快释放内存,建议从每个MutationRecord 中抽取最有用的信息,保存在一个新对象中,最后抛弃MutationRecord

14.2.4 结合现代技术的最佳实践与场景

1. 性能监控与错误追踪

在现代APM(Application Performance Monitoring)工具中,MutationObserver 用于监控DOM的稳定性,检测意外的布局抖动,或者追踪特定元素的出现与消失,辅助排查问题。

// 监控一个可能无限循环添加节点的错误
let mutationCount = 0;
const observer = new MutationObserver((mutations) => {
  mutationCount += mutations.length;
  if (mutationCount > 1000) {
    console.error('疑似DOM操作无限循环!');
    observer.disconnect();
  }
});
observer.observe(document.body, { childList: true, subtree: true });
2. 第三方库/组件开发

当你开发的组件需要嵌入到不可控的宿主页面时,你不能要求用户按照你的方式初始化。使用 MutationObserver 可以自动初始化出现在DOM中的组件。

// 自动初始化所有带有 `data-widget` 属性的元素
class MyWidget {
  constructor(element) { /* ... */ }
}

const widgetObserver = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.type === 'childList') {
      mutation.addedNodes.forEach((node) => {
        // 确保是元素节点并且有 data-widget 属性
        if (node.nodeType === 1 && node.dataset.widget) {
          new MyWidget(node);
        }
        // 同时检查新增节点的后代
        if (node.querySelectorAll) {
          node.querySelectorAll('[data-widget]').forEach(el => new MyWidget(el));
        }
      });
    }
  });
});

widgetObserver.observe(document.documentElement, {
  childList: true,
  subtree: true
});
3. 与现代前端框架协同工作

虽然Vue/React有自己的响应式系统,但在某些边缘情况下,MutationObserver 依然有用武之地:

  • 集成非框架代码: 当页面中混用了框架和传统JS库,并且你需要响应传统库造成的DOM变化时。
  • 富文本编辑器: 编辑器核心通常重度依赖 MutationObserver 来追踪用户的输入和格式变化,这与使用哪个框架无关。
  • “无限滚动”列表: 监听占位符元素是否进入视口,从而加载更多数据。
4. 浏览器扩展(Chrome Extension)

内容脚本(Content Script)经常使用 MutationObserver 来检测和修改动态加载的页面内容,例如为AJAX加载的按钮添加新功能。

// 在某个社交网站上,为动态加载的新帖子自动添加一个“翻译”按钮
const postObserver = new MutationObserver((mutations) => {
  mutations.forEach((mutation) => {
    if (mutation.type === 'childList') {
      mutation.addedNodes.forEach((node) => {
        if (node.nodeType === 1 && node.matches('.post')) {
          addTranslateButton(node);
        }
      });
    }
  });
});

postObserver.observe(document.getElementById('feed'), {
  childList: true,
  subtree: true
});

14.2.5 MutationObserver学习总结

  • 性能第一: 始终使用最严格的配置。能用 attributeFilter 就不要监听所有属性;如果只需要监听直接子节点,就不要开启 subtree
  • 及时清理: 在组件、页面卸载时,务必调用 .disconnect()。这是防止内存泄漏的铁律。
  • 理解异步性: 回调是微任务,这意味着它会在当前同步代码和Promise回调之后、浏览器渲染之前执行。
  • 并非万能: 对于高频、大量的DOM操作,即使使用 MutationObserver,回调函数本身的逻辑如果过于复杂,依然可能成为性能瓶颈。此时应考虑使用防抖(debounce)或节流(throttle)技术来优化回调逻辑。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐