MutationObserver 用法介绍

Mutation Event

DOM3 中定义了一系列用于监听 DOM 树结构变化的事件:

  • DOMAttrModified
  • DOMCharacterDataModified
  • DOMNodeInserted
  • DOMNodeInsertedIntoDocument
  • DOMNodeRemoved
  • DOMNodeRemovedFromDocument
  • DOMSubtreeModified
1
element.addEventListener('DOMAttrModified', (event) => {}, false);

但由于性能和兼容性问题,在 DOM4 中提议用 Mutation Observes 取代 Mutation Events

  • 性能问题
    • 为 DOM 添加 mutation 监听器极度降低进一步修改 DOM 文档的性能(慢 1.5 - 7 倍),此外, 移除监听器不会逆转损害
      • Mutation Events 是同步执行的,每次调用都需要从事件队列中取出事件,执行,然后事件队列中移除,期间需要移动队列元素。如果事件触发比较频繁,那么浏览器有可能被变慢
      • Mutation Events 本身也是事件,在冒泡阶段如果触发了其他 Mutation Events,有可能会导致阻塞线程
  • 兼容性问题
    • IE9 之前版本不支持
    • IE9 - IE11、IE edge 和 Firefox 不支持 DOMNodeInsertedIntoDocumentDOMNodeRemovedFromDocument
    • WebKit 不支持 DOMAttrModified

Mutation Observer

Mutation Observer 是在 DOM4 中定义的,用于替代 Mutation Events 的新 API。它和 Mutation Events 有所不同,所有监听操作以及回调是在其它脚本执行完成之后异步执行,并且是所有变动触发之后,将变更记录在数组中,统一进行回调。如果监听多个 DOM 变化且这个 DOM 都发生了变化,那么会将这些变化记录在变更数组中,所有变动结束后执行对应的回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Constructor(MutationCallback callback), Exposed=Window]
interface MutationObserver {
void observe(Node target, optional MutationObserverInit options);
void disconnect();
sequence<MutationRecord> takeRecords();
};

callback MutationCallback = void (sequence<MutationRecord> mutations, MutationObserver observer);

dictionary MutationObserverInit {
boolean childList = false;
boolean attributes;
boolean characterData;
boolean subtree = false;
boolean attributeOldValue;
boolean characterDataOldValue;
sequence<DOMString> attributeFilter;
};
1
2
3
4
5
const observer = new MutationObserver((mutations, _observer) => {
mutations.forEach(mutation => {
console.info(mutation.type);
});
});

API

  • observe()

    注册待观察的目标节点

    1
    observer.observe(target, options);

    observe() 与 addEventListener() 类似,对同一目标节点观察多次且回调对象一致,不会触发多次回调。

    options 是 MutationObserverInit 对象,指定观察的变动类型:

    • childList

      boolean,观察目标节点的子节点(新增、移除)是否发生变化

    • attributes

      boolean,观察目标节点的属性(新增、修改、删除成功)是否发生变化

    • characterData

      boolean,观察目标节点的 characterData 节点(一种抽象接口,具体可包括文本节点、注释节点以及处理指令节点)的文本内容是否发生变化

    • subtree

      boolean,观察目标节点及后代节点的 childListattributeschartacterData 是否发生变化

    • attributeOldValue

      boolean,在 attributes 设置为 true 时,如果目标节点属性发生变动,则将变动前的值记录到 MutationRecord 对象的 oldValue 属性中

    • characterDataValue

      boolean,在 characterData 设置为 true 时,如果目标节点的 characterData 节点的文本内容发生变动,则将变动前的值记录到 MutationRecord 对象的 oldValue 属性中

    • attributeFilter

      array,属性名数组,包含需要观察的属性名;未在数组内的属性,即使发生变动也不会触发回调

    childListattributes 或者 characterData 三个属性中必须至少有一个为 true,否则会抛出异常 “An invalid or illegal string was specified”。

  • disconnect()

    停止观察目标节点

    1
    observer.disconnect()
  • takeRecords()

    清空并返回目标节点的变化记录(MutationRecord)数组

    1
    observer.takeRecords()

    MutationRecord

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    [Exposed=Window]
    interface MutationRecord {
    readonly attribute DOMString type;
    [SameObject] readonly attribute Node target;
    [SameObject] readonly attribute NodeList addedNodes;
    [SameObject] readonly attribute NodeList removedNodes;
    readonly attribute Node? previousSibling;
    readonly attribute Node? nextSibling;
    readonly attribute DOMString? attributeName;
    readonly attribute DOMString? attributeNamespace;
    readonly attribute DOMString? oldValue;
    };
    • type

      返回 attributescharacterDatachildList 中的一种

    • target

      返回受变动影响的节点,具体的返回的节点类型依赖于 type 值;type 为 attributes 时返回发生变动的属性节点所在的元素节点;type 为 characterData 时返回发生变动的 characterData 节点;type 为 childList 时返回发生变动的子节点的父节点

    • addedNodes

      返回被添加的节点或 null

    • removeNodes

      返回被删除的节点或 null

    • previousSibling

      返回被添加或被删除的节点的前一个兄弟节点或 null

    • nextSibling

      返回被添加或被删除的节点的后一个兄弟节点或 null

    • attributeName

      返回发生变动的属性名或 null

    • attributeNamespace

      返回发生变动的命名空间或 null

    • oldValue

      返回内容依赖于 type 值:type 为 attributes 时返回变动前的属性值;type 为 characterData 时返回变动前的 characterData 节点内容;type 为 childList 时返回 null

实例

  • 观察 childList

    1
    2
    3
    4
    5
    6
    7
    observer.observe(target, { childList: true });

    target.appendChild(document.createElement("div")); // 添加了一个元素子节点,触发回调函数
    target.appendChild(document.createTextNode("foo")); // 添加了一个文本子节点,触发回调函数
    target.removeChild(target.childNodes[0]); // 移除第一个子节点,触发回调函数

    target.childNodes[0].appendChild(document.createElement("div")); // 为第一个子节点添加一个子节点,不会触发回调函数;如果需要触发,则需要设置 subtree 属性为 true
  • 观察 characterData

    1
    2
    3
    4
    5
    observer.observe(target, { characterData: true });
    target.childNodes[0].data = "bar"; // 不会触发回调函数,因为发生变化的是 target 的子节点

    observer.observe(target, { characterData: true, subtree: true });
    target.childNodes[0].data = "bar"; // 触发回调函数
  • 观察 attributes

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    observer.observe(target, { attributes:true });

    target.setAttribute("foo", "bar"); // 不管 foo 属性是否存在,都会触发回调函数
    target.setAttribute("foo", "bar"); // 即使前后两次的属性值一样,还是会触发回调函数
    target.removeAttribute("foo"); // 移除 foo 属性节点,触发回调函数
    target.removeAttribute("foo"); // 不会触发回调函数,因为 target 没有这个属性

    observer.observe(target, { attributes:true, attributeFilter: ["bar"] }); // 指定观察属性名
    target.setAttribute("foo", "bar"); // 不会触发回调函数,因为不监听 foo 属性
    target.setAttribute("bar", "foo"); // 触发回调函数

参考