上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
返回列表 发新帖

⭐ [源码解析|溯水] MutationObserver 快速上手

[复制链接]
  • TA的每日心情
    奋斗
    2025-3-1 19:55
  • 签到天数: 6 天

    [LV.2]偶尔看看I

    10

    主题

    11

    回帖

    124

    积分

    荣誉开发者

    积分
    124

    油中2周年新人报道荣誉开发者

    发表于 2025-2-25 20:36:07 | 显示全部楼层 | 阅读模式

    本帖最后由 溯水流光 于 2025-2-25 21:14 编辑

    [源码解析|溯水] MutationObserver 快速上手

    本文是《从 0 到 1,手把手带你剖析异步查询库 elmGetter 的源码,进行深度定制和二次开发,附 MutationObserver 讲解》系列文章中的一篇。

    该系列文章目录网址为:

    https://bbs.tampermonkey.net.cn/thread-8196-1-1.html

    作者: 溯水流光 (脚本猫论坛, 油猴中文网)

    演练场景的准备

    接下来我们将准备一个演练场景, 用来测试我们写的脚本

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>playground</title>
    </head>
    <body>
      <button onclick="addComment()">addComment</button>
    
      <ul class="comments">
        <li class="comment">第1条评论</li>
      </ul>
    
      <div class="app"></div>
    
      <script>
        setTimeout(() => {
          const appElement = document.querySelector('.app');
    
          const pElement = document.createElement('p');
    
          pElement.className = 'child';
    
          pElement.textContent = 'child';
    
          appElement.appendChild(pElement);
        }, 1000);
    
        function addComment() {
          const commentsEl = document.querySelector('.comments');
    
          const liElement = document.createElement('li');
    
          liElement.className = 'comment';
    
          liElement.textContent = `第${commentsEl.children.length + 1}条评论`;
    
          commentsEl.appendChild(liElement);
        }
      </script>
    </body>
    </html>

    VsCode 打开所在目录, 右键VsCode插件LiveServer跑起来

    203312i26sccdrvcmcrcyr.png 203337haslqbqza1bby6q1.png

    .child元素是用于模拟Ajax请求

    addComment是模拟评论区懒加载, 也就是滚动到了底部才加载下一页的评论

    .comments是一个ul, 代表评论区, 用于存储评论

    .comment是一个li, 代表一条评论

    MutationObserver

    资料参考:

    DOM - Mutation Observer API - 《阮一峰 JavaScript 教程
    https://www.bookstack.cn/read/javascript-tutorial/docs-dom-mutationobserver.md

    目标

    实现对演练场景进行修改, 将每一条.comment的内容添加上, Ciallo~(∠・ω< )⌒★

    思路

    我们最先想到的解决方案是setInterval+querySelectorAll, 这个解决办法是可以实现的, 也是经典的异步查询库的实现, 具体可以翻阅附录AB. 这个方案最大的缺点就是效率低, 性能低, 如果异步查询过多, 容易卡顿.

    但我们有了新的更加强大和高效的解决: MotationObserve.

    参考引用:

    Mutation Observer API 用来监视 DOM 变动。DOM 的任何变动,比如节点的增减、属性的变动、文本内容的变动,这个 API 都可以得到通知。

    概念上,它很接近事件,可以理解为 DOM 发生变动就会触发 Mutation Observer 事件。但是,它与事件有一个本质不同:事件是同步触发,也就是说,DOM 的变动立刻会触发相应的事件;Mutation Observer 则是异步触发,DOM 的变动并不会马上触发,而是要等到当前所有 DOM 操作都结束才触发。

    这样设计是为了应付 DOM 变动频繁的特点。举例来说,如果文档中连续插入1000个<p>元素,就会连续触发1000个插入事件,执行每个事件的回调函数,这很可能造成浏览器的卡顿;而 Mutation Observer 完全不同,只在1000个段落都插入结束后才会触发,而且只触发一次。

    Mutation Observer 有以下特点。

    • 它等待所有脚本任务完成后,才会运行(即异步触发方式)。
    • 它把 DOM 变动记录封装成一个数组进行处理,而不是一条条个别处理 DOM 变动。
    • 它既可以观察 DOM 的所有类型变动,也可以指定只观察某一类变动。

    快速上手 第一小节

    // ==UserScript==
    // @name         New Userscript
    // @namespace    http://tampermonkey.net/
    // @version      2025-02-21
    // @description  try to take over the world!
    // @author       You
    // @match        http://127.0.0.1:5500/playground.html
    // @icon         https://www.google.com/s2/favicons?sz=64&domain=0.1
    // @grant        unsafeWindow
    // ==/UserScript==
    
    (async function () {
        'use strict';
    
        // 创建一个观察者
        let observer = new MutationObserver(() => {
           console.log("评论区的DOM结构发生了变化, 可能增加, 修改了或删除了评论区的子元素");
        });
    
        // 获取目标对象
        let targetEl = document.querySelector(".comments");
    
        // 让观察者开始监听目标元素的变化
        observer.observe(targetEl, {
            childList: true, // 监听其直接子元素的增加, 修改, 删除
            subtree: true // 监听其所有后代元素
        });
    })();
    

    做些实验

    小技巧: 选中元素后, 开发者工具的控制台, 用$0可以打印出当前选择的元素

    203407fzzvmrmbvvjr7a77.png

    试一试

    注释掉下面的代码

    subtree: true // 监听其所有后代元素, 需先指定 childList 为 true

    注释前:

    203432eh6uv694u6uhqhcp.png

    注释后:

    203450fe890oyo9ei3sesd.png

    参考引用

    观察器所能观察的 DOM 变动类型(即上面代码的options对象),有以下几种。

    • childList:子节点的变动(指新增,删除或者更改)。
    • attributes:属性的变动。
    • characterData:节点内容或节点文本的变动。

    想要观察哪一种变动类型,就在option对象中指定它的值为true。需要注意的是,至少必须同时指定这三种观察的一种,若均未指定将报错。

    快速上手 第二小节 取消观察

    GM_registerMenuCommand 参考: 油猴中文开发者文档:

    https://learn.scriptcat.org/%E6%B2%B9%E7%8C%B4%E6%95%99%E7%A8%8B/%E5%85%A5%E9%97%A8%E7%AF%87/%E5%8F%B3%E9%94%AE%E8%8F%9C%E5%8D%95%E4%B8%8EGM%E5%AD%98%E5%82%A8%E5%87%BD%E6%95%B0%E4%BB%8B%E7%BB%8D/

    // ==UserScript==
    // @name         New Userscript
    // @namespace    http://tampermonkey.net/
    // @version      2025-02-21
    // @description  try to take over the world!
    // @author       You
    // @match        http://127.0.0.1:5500/playground.html
    // @icon         https://www.google.com/s2/favicons?sz=64&domain=0.1
    // @grant        unsafeWindow
    // @grant       GM_registerMenuCommand
    // ==/UserScript==
    
    (async function () {
        'use strict';
    
        // 创建一个观察者
        let observer = new MutationObserver(() => {
            console.log("评论区的DOM结构发生了变化, 可能增加, 修改了或删除了评论区的子元素");
        });
    
        // 获取目标对象
        let targetEl = document.querySelector(".comments");
    
        // 让观察者开始监听目标元素的变化
        observer.observe(targetEl, {
            childList: true, // 监听其直接子元素的增加, 修改, 删除
            subtree: true // 监听其所有后代元素
        });
    
        GM_registerMenuCommand("取消监听评论区", () => {
            observer.disconnect(); // 让监听器取消监听
        })
    })();
    
    203513zaqha0qi5aueaept.png

    快速上手 第三小节 回调参数

    参考引用:
    DOM 每次发生变化,就会生成一条变动记录(MutationRecord 实例)。该实例包含了与变动相关的所有信息

    observe观察者, 将会把这个变动记录(mutations), 作为参数传给我们的回调函数

    let observer = new MutationObserver(mutations => {
            // ...
        });

    参考引用:

    MutationRecord对象包含了DOM的相关信息,有如下属性:

    • type:观察的变动类型(attributescharacterData或者childList)。
    • target:发生变动的DOM节点。
    • addedNodes:新增的DOM节点。
    • removedNodes:删除的DOM节点。
    • previousSibling:前一个同级节点,如果没有则返回null
    • nextSibling:下一个同级节点,如果没有则返回null
    • attributeName:发生变动的属性。如果设置了attributeFilter,则只返回预先指定的属性。
    • oldValue:变动前的值。这个属性只对attributecharacterData变动有效,如果发生
    // ==UserScript==
    // @name         New Userscript
    // @namespace    http://tampermonkey.net/
    // @version      2025-02-21
    // @description  try to take over the world!
    // @author       You
    // @match        http://127.0.0.1:5500/playground.html
    // @icon         https://www.google.com/s2/favicons?sz=64&domain=0.1
    // @grant        unsafeWindow
    // @grant       GM_registerMenuCommand
    // ==/UserScript==
    
    (async function () {
        'use strict';
    
        // 创建一个观察者
        let observer = new MutationObserver(mutations => {
            for (const mutation of mutations) {
                if (mutation.type === 'attributes') {
                    console.log(mutation.target, "元素的属性发生了改变");
                }
                for (const node of mutation.addedNodes) {
                    if (node instanceof Element) {
                        console.log(mutation.target, "添加了", node, "元素");
                    } else {
                        console.log(mutation.target, "添加了", node, "文本或注释");
                    }
                }
            }
        });
    
        // 获取目标对象
        let targetEl = document.querySelector(".comments");
    
        // 让观察者开始监听目标元素的变化
        observer.observe(targetEl, {
            childList: true, // 监听其直接子元素的增加, 修改, 删除
            subtree: true, // 监听其所有后代元素
            attributes: true, // 监听属性变化
        });
    
        GM_registerMenuCommand("取消监听评论区", () => {
            observer.disconnect(); // 让监听器取消监听
        })
    })();
    
    203532d7er1f8chix8zez9.png 203545y53zt3f1b51zcoh3.png

    快速上手 收尾 Ciallo~(∠・ω< )⌒★

    // ==UserScript==
    // @name         New Userscript
    // @namespace    http://tampermonkey.net/
    // @version      2025-02-21
    // @description  try to take over the world!
    // @author       You
    // @match        http://127.0.0.1:5500/playground.html
    // @icon         https://www.google.com/s2/favicons?sz=64&domain=0.1
    // @grant        unsafeWindow
    // @grant       GM_registerMenuCommand
    // ==/UserScript==
    
    (async function () {
        'use strict';
    
        let commentsEl = document.querySelector(".comments");
    
        function addText(node) {
            node.textContent = node.textContent + " Ciallo~(∠・ω< )⌒★";
        }
    
        commentsEl.querySelectorAll(".comment").forEach(function (node) {
            addText(node);
        });
    
        // 创建一个观察者
        let observer = new MutationObserver(mutations => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node instanceof Element) {
                        addText(node);
                    }
                }
            }
        });
    
        // 让观察者开始监听目标元素的变化
        observer.observe(commentsEl, {
            childList: true, // 监听其直接子元素的增加, 修改, 删除
        });
    
        GM_registerMenuCommand("取消监听评论区", () => {
            observer.disconnect(); // 让监听器取消监听
        })
    })();
    
    203601f1i6bannk8mv1ahx.png

    Ciallo~(∠・ω< )⌒★, 以上就是MutationObserver的全部内容, 如果上面代码都看得懂, 阅读elmGetter的源码就没有问题了

    下一章节: <正文: elmGetter的源码解析>

    文章地址: https://bbs.tampermonkey.net.cn/thread-8198-1-1.html

    发表回复

    本版积分规则

    快速回复 返回顶部 返回列表