本帖最后由 溯水流光 于 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
跑起来
.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
可以打印出当前选择的元素
试一试
注释掉下面的代码
subtree: true // 监听其所有后代元素, 需先指定 childList 为 true
注释前:
注释后:
参考引用
观察器所能观察的 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(); // 让监听器取消监听
})
})();
快速上手 第三小节 回调参数
参考引用:
DOM 每次发生变化,就会生成一条变动记录(MutationRecord 实例)。该实例包含了与变动相关的所有信息
observe观察者, 将会把这个变动记录(mutations), 作为参数传给我们的回调函数
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
变动有效,如果发生
// ==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(); // 让监听器取消监听
})
})();
快速上手 收尾 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(); // 让监听器取消监听
})
})();
Ciallo~(∠・ω< )⌒★, 以上就是MutationObserver的全部内容, 如果上面代码都看得懂, 阅读elmGetter的源码就没有问题了
下一章节: <正文: elmGetter的源码解析>
文章地址: https://bbs.tampermonkey.net.cn/thread-8198-1-1.html