DOM总结(2.MutationObserver)

186 阅读6分钟

MutationObserver接口可以观察整个文档

使用MutationObserver接口可以观察整个文档、DOM 树的一部分,或某个元素的变化。

1 基本用法

通过调用MutationObserver构造函数并传入一个回调函数来创建MutationObserver的实例。

let observer = new MutationObserver(() => console.log('changed!'));

1.1 observe()方法

使用该方法与DOM关联起来才能检测DOM的变化,该方法接收两个参数:要观察其变化的DOM节点,以及一个 MutationObserverInit对象。
MutationObserverInit 对象用于控制观察哪些方面的变化,是一个键/值对形式配置选项的字典。

let observer = new MutationObserver(() => console.log(' attributes changed'));
observer.observe(document.body, { attributes: true });

上述代码创建了一个观察者observer并让它观察</body>元素上的属性变化。当</body>上的任何属性发生变化都会被observer发现,然后异步执行注册的回调函数。
MutationObserverInit对象的属性及说明:

属性说明
subtree布尔值,表示除了观测目标结点,是否观察目标节点的子树(后代),如果是 false,则只观察目标节点的变化;如果是 true,则观察目标节点及其整个子树 默认为 false
attributes布尔值,表示是否观察目标节点的属性变化 默认为 false
attributeFilter字符串数组,表示要观察哪些属性的变化,该值为true时也会将 attributes 的值转换为 true,默认为观察所有属性
attributeOldValue布尔值,表示 MutationRecord 是否记录变化之前的属性值 把这个值设置为 true 也会将 attributes 的值转换为 true 默认为 false
characterData布尔值,表示修改字符数据是否触发变化事件 默认为 false
characterDataOldValue布尔值,表示 MutationRecord 是否记录变化之前的字符数据 把这个值设置为 true 也会将 characterData 的值转换为 true 默认为 false
childList布尔值,表示修改目标节点的子节点是否触发变化事件 默认为 false

注:在调用observe()时,MutationObserverInit 对象中的 attribute、characterData 和 childList 属性必须至少有一项为true(无论是直接设置这几个属性,还是通过设置 attributeOldValue 等属性间接导致它们的值转换为 true)。否则会抛出错误,因为没 有任何变化事件可能触发回调。

1.2 MutationRecord

每个回调都会收到一个 MutationRecord 实例的数组。MutationRecord实例包含的信息包括发生了什么变化,以及DOM的哪一部分受到了影响。因为回调执行之前可能同时发生多个满足观察条件的事件,所以每次执行回调都会传入一个包含按顺序入队的MutationRecord 实例的数组。 下面展示了反映一个属性变化的MutationRecord实例的数组:

        let observer = new MutationObserver((mutationRecords) => {
            console.log(mutationRecords);
        })
        observer.observe(document.body, {
            attributes: true
        })
        document.body.setAttribute('first', 'aaa');
        document.body.setAttribute('first', 'bbb');
        document.body.setAttribute('first', 'ccc');

回调执行的结果如下所示:

image.png MutationRecord实例的属性及说明:

属性说明
target被修改影响的目标节点
type字符串,表示变化的类型:"attributes"、"characterData"或"childList"
oldValue当attributeOldValue或characterDataOldValue为 true,该属性值为被替代的值
attributeName对于"attributes"类型的变化,这里保存被修改属性的名字 其他变化事件会将这个属性设置为 null
target对于使用了命名空间的"attributes"类型的变化,这里保存被修改属性的名字 其他变化事件会将这个属性设置为 null
attributeNamespace被修改影响的目标节点
addedNodes对于"childList"类型的变化,返回包含变化中添加节点的 NodeList 默认为空 NodeList
removedNodes对于"childList"类型的变化,返回包含变化中删除节点的 NodeList 默认为空 NodeList
previousSibling对于"childList"类型的变化,返回变化节点的前一个同胞 Node 默认为 null
nextSibling对于"childList"类型的变化,返回变化节点的后一个同胞 Node 默认为 null

回调函数的第二个参数是观察变化的MutationObserver的实例

1.3 disconnect()方法

执行该方法不仅会停止此后变化事件的回调,也会抛弃已经加入任务队列要异步执行的回调

        let observer = new MutationObserver((mutationRecords, mutationObserver) => {
            console.log(mutationRecords);
        })
        observer.observe(document.body, {
            attributes: true
        })
        document.body.setAttribute('first', 'aaa');
        observer.disconnect();
        document.body.setAttribute('first', 'bbb');

上面的代码没有任何输出

1.4 复用和重用MutationObserver

复用

可以复用一个MutationObserver的实例观察多个不同的目标结点,如:

        observer.observe(childA, { attributes: true }); 
        observer.observe(childB, { attributes: true });

上面的代码使用observer观察了childA节点和childB节点的属性变化情况。disconnect()方法是一个“一刀切”的方案,调用它会停止观察所有目标。

重用

调用disconnect()并不会结束MutationObserver的生命。还可以重新使用这个观察者,再将它关联到新的目标节点。下面的示例在两个连续的异步块中先断开然后又恢复了观察者与元素的关联:

        let observer = new MutationObserver((mutationRecords, mutationObserver) => {
            console.log(mutationRecords);
        })
        observer.observe(document.body, {
                attributes: true
            })
            // 这行代码会触发变化事件
        document.body.setAttribute('first', 'aaa');
        setTimeout(() => {
            observer.disconnect();
            // 这行代码不会触发变化事件
            document.body.setAttribute('first', 'bbb');
        }, 0);
        setTimeout(() => {
            observer.observe(document.body, {
                    attributes: true
                })
                // 这行代码会触发变化事件
            document.body.setAttribute('first', 'ccc');
        }, 0);

2 异步回调与记录队列

每次观察的目标发生变化时,变化的信息会保存在MutationRecord 实例中,然后添加到记录队列。这个队列对每个MutationObserver实例都是唯一的,是所有DOM变化事件的有序列表。

记录队列

每次MutationRecord被添加到 MutationObserver的记录队列时,仅当之前没有已排期的微任务回调时(队列中微任务长度为0),才会将观察者注册的回调(在初始化MutationObserver时传入) 作为微任务调度到任务队列上。这样可以保证记录队列的内容不会被回调处理两次。回调会接收到一 个MutationRecord实例的数组,顺序为它们进入记录队列的顺序。回调执行后,记录队列会被清空。

takeRecords()方法

调用MutationObserver实例的takeRecords()方法可以清空记录队列,取出并返回其中的所有MutationRecord实例。

        let observer = new MutationObserver((mutationRecords, mutationObserver) => {
            console.log(mutationRecords, mutationObserver === observer);
        })
        observer.observe(document.body, {
            attributes: true
        })
        document.body.setAttribute('first', 'aaa');
        document.body.setAttribute('first', 'bbb');
        document.body.setAttribute('first', 'ccc');
        console.log(observer.takeRecords());
        console.log(observer.takeRecords());

image.png
这在希望断开与观察目标的联系,但又希望处理由于调用 disconnect()而被抛弃的记录队列中的 MutationRecord 实例时比较有用。

3 性能、内存与垃圾回收

MutationObserver 的引用

MutationObserver拥有对要观察的目标节点的弱引用,因为是弱引用,所以不会妨碍垃圾回收程序回收目标节点。
而目标节点却拥有对MutationObserver的强引用,如果目标节点从 DOM 中被移除,随后 被垃圾回收,则关联的 MutationObserver 也会被垃圾回收

MutationRecord 的引用

MutationRecord对至少对一个DOM节点的引用,如果变化是 childList 类型,则会包含多个节点的引用。因此任务队列保存这些MutationRecord 实例会妨碍DOM节点被回收。记录队列和回调处理的默认行为是耗尽这个队列,处理每个 MutationRecord,然后让它们超出作用域并被垃圾回收。如果需要尽快地释放内存,建议从每个 MutationRecord 中抽取出最有用的信息,然后保存到一个新对象中,最后抛弃 MutationRecord。