MutationObserver

597 阅读4分钟

前言

MutationObserver 是一个dom观察器,当dom发生改变前(willchanged),会执行其回调函数,自己观察会发现不少三方UI组件用到了

其会将回调添加到当前执行栈的末尾,且在其他异步任务之前执行((如I/O、计时器等)),对于高性能低延迟的场景非常好用,也避免了延迟调用出现的卡顿问题

MutationObserver

MutationObserver 创建观察很简单,直接 new MutationObserver 即可

new MutationObserver 创建监听回调

会在观察内容发生改变操作前,回调该函数,因为打印时会发件 target 节点的 childNodes 节点没有变化

第一个参数为变化数组,会按照顺序记录先后的改变操作(增删等),observer为我们的观察对象,可以取消、观察

let obser = new MutationObserver(
    (mutations: MutationRecord[], observer: MutationObserver) => {
        console.log(mutations);
    }
);

回调返回值大致结构如下所示,主要用到第一个参数,第二个参数后面介绍,可用可不用

QQ_1724749432698.png

observer 开启监听,设置监听规则

通过调用其 observer 函数来启用监听,可以设置 config 参数,一般用 childList、subtree、characterDataOldValue 就足够了,参数使用如下所示

const dom = document.getElementById("pppp");

obser.observe(dom!, {
    childList: true
});

config 属性如下所示,根据自己需要取即可

/** 设置一个属性本地名(不包含命名空间)的列表,如果并非所有属性更改都需要被观察,并且 attributes 为 true 或被省略。 */ 
attributeFilter?: string[]; 
/** 设置为 true,如果 attributes 为 true 或被省略,并且需要记录目标属性更改前的值。 */ 
attributeOldValue?: boolean; 
/** 设置为 true,如果要观察目标属性的更改。如果指定了 attributeOldValue 或 attributeFilter,则可以省略此属性。 */
attributes?: boolean; 
/** 设置为 true,如果要观察目标数据的更改。如果指定了 characterDataOldValue,则可以省略此属性。 */
characterData?: boolean; 
/** 设置为 true,如果 characterData 为 true 或被省略,并且需要记录目标更改前的数据。 */ 
characterDataOldValue?: boolean; 
/** 设置为 true,如果要观察目标子节点的更改。 */
childList?: boolean; 
/** 设置为 true,如果要观察不只是目标,还有目标后代的更改。 */
subtree?: boolean;

disconnect取消监听

通过 disconnect 取消监听,有时为了避免不必要的监听,可以在合适的时机取消监听,在重新开启监听

obser.disconnect()

案例(让被删除节点恢复案例,部分水印防伪功能)

使用 MutationObserver 监听一个 pppp 的节点,检测到其删除操作后,我们给某个节点主动添加回来,一些水印防伪功能会用到,我们就做一个简易的防删除案例

//查找dom,用于监听其子节点
const dom = document.getElementById("pppp");

//设置改变前的回调
const obser = new MutationObserver(
    (mutations: MutationRecord[], observer: MutationObserver) => {
        mutations.forEach((node) => {
            node.removedNodes.forEach((node) => {
                dom?.appendChild(node);
            });
        });
    }
);
//添加回调
obser.observe(dom!, {
    childList: true
});

这样完成了一个删除后的恢复功能,也可以根据需要调用 disconnect、observe方法,如果不想由于回调内部操作再次触发回调,需要这样做

例如:上面的删除后的追加操作,如果里面是不是单纯追加,而是重置操作,可能还有删除和追加操作,则再次删除操作可能会触发回调地狱,需要额外添加判断条件,如果使用 disconnect + observe则可以有效避免

此外,可能还会有很多情况,有时需要在操作的末尾,可能还会有其他dom操作,要延迟恢复监听的话,可以通过setTimeout 重新启动一个宏队列任务避免提前开启监听

毕竟其回调会被追加到当前执行栈的末尾,可以通过 setTimeout 会重新启动一个宏队列任务(优先级在执行中的微队列任务之后),以此解决监听时机问题

const obser = new MutationObserver(
        (mutations: MutationRecord[], observer: MutationObserver) => {
        observer.disconnect();
        
        ...我们的操作
        
        observer.observe(dom!, {
            childList: true,
        });
        //开启一个新的
        // setTimeout(() => {
        //     observer.observe(dom!, {
        //         childList: true,
        //     });
        // }, 0);
    }
);

整体代码

function App() {
    useEffect(() => {
        const dom = document.getElementById("pppp");
        //改变前的回调
        const obser = new MutationObserver(
            (mutations: MutationRecord[], observer: MutationObserver) => {
                console.log(mutations);
                observer.disconnect();
                mutations.forEach((node) => {
                    node.removedNodes.forEach((node) => {
                        dom?.appendChild(node);
                    });
                });
                observer.observe(dom!, {
                    childList: true,
                });
                // setTimeout(() => {
                //     observer.observe(dom!, {
                //         childList: true,
                //     });
                // }, 0);
            }
        );
        obser.observe(dom!, {
            childList: true,
        });
    }, []);

    return (
        <div className="App">
            <header className="App-header">
                <img src={logo} className="App-logo" alt="logo" />
                <p id="pppp">
                    Edit <code>src/App.tsx</code> and save to reload.
                </p>
            </header>
        </div>
    );
}

最后

自己也可以尝试将添加的文本信息换个颜色🤣