溯水流光 发表于 2025-2-25 20:36:07

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

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

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

> 本文是《从 0 到 1,手把手带你剖析异步查询库 elmGetter 的源码,进行深度定制和二次开发,附 MutationObserver 讲解》系列文章中的一篇。
>
> 该系列文章目录网址为:
>
> https://bbs.tampermonkey.net.cn/thread-8196-1-1.html
>
> 作者: 溯水流光 (脚本猫论坛,油猴中文网)

## 演练场景的准备

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

```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`跑起来

<img src="data/attachment/forum/202502/25/203312i26sccdrvcmcrcyr.png" width="300" />

<img src="data/attachment/forum/202502/25/203337haslqbqza1bby6q1.png" width="500" />


`.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 的所有类型变动,也可以指定只观察某一类变动。

### 快速上手 第一小节

```js
// ==UserScript==
// @name         New Userscript
// @namespace    http://tampermonkey.net/
// @version      2025-02-21
// @descriptiontry 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`可以打印出当前选择的元素

<img src="data/attachment/forum/202502/25/203407fzzvmrmbvvjr7a77.png" width="750" />


#### 试一试

注释掉下面的代码

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

注释前:
<img src="data/attachment/forum/202502/25/203432eh6uv694u6uhqhcp.png" width="750" />

注释后:
<img src="data/attachment/forum/202502/25/203450fe890oyo9ei3sesd.png" width="750" />

#### 参考引用

> 观察器所能观察的 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/

```js
// ==UserScript==
// @name         New Userscript
// @namespace    http://tampermonkey.net/
// @version      2025-02-21
// @descriptiontry 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(); // 让监听器取消监听
    })
})();

```
<img src="data/attachment/forum/202502/25/203513zaqha0qi5aueaept.png" width="500" />


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

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

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

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

> 参考引用:
>
> `MutationRecord`对象包含了DOM的相关信息,有如下属性:
>
> - `type`:观察的变动类型(`attributes`、`characterData`或者`childList`)。
> - `target`:发生变动的DOM节点。
> - `addedNodes`:新增的DOM节点。
> - `removedNodes`:删除的DOM节点。
> - `previousSibling`:前一个同级节点,如果没有则返回`null`。
> - `nextSibling`:下一个同级节点,如果没有则返回`null`。
> - `attributeName`:发生变动的属性。如果设置了`attributeFilter`,则只返回预先指定的属性。
> - `oldValue`:变动前的值。这个属性只对`attribute`和`characterData`变动有效,如果发生

```js
// ==UserScript==
// @name         New Userscript
// @namespace    http://tampermonkey.net/
// @version      2025-02-21
// @descriptiontry 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(); // 让监听器取消监听
    })
})();

```

<img src="data/attachment/forum/202502/25/203532d7er1f8chix8zez9.png" width="750" />

<img src="data/attachment/forum/202502/25/203545y53zt3f1b51zcoh3.png" width="750" />


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

```js
// ==UserScript==
// @name         New Userscript
// @namespace    http://tampermonkey.net/
// @version      2025-02-21
// @descriptiontry 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(); // 让监听器取消监听
    })
})();

```

<img src="data/attachment/forum/202502/25/203601f1i6bannk8mv1ahx.png" width="500" />


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



> 下一章节: <正文: elmGetter的源码解析>
>
> 文章地址: https://bbs.tampermonkey.net.cn/thread-8198-1-1.html
页: [1]
查看完整版本: ⭐ [源码解析|溯水] MutationObserver 快速上手