1. 什么是MutationObserver?
MutationObserver 接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。
MutationObserver是一个异步操作,属于微任务,在Vue源码中实现nextTick的调度机制使用到了这个。为什么是异步操作就不用说了吧。
-
构造函数
MutationObserver()
创建并返回一个新的 MutationObserver 它会在指定的 DOM 发生变化时被调用。
// 创建一个观察器实例并传入回调函数 const observer = new MutationObserver(callback); -
方法
disconnect()
阻止 MutationObserver实例继续接收的通知,直到再次调用其 observe() 方法,该观察者对象包含的回调函数都不会再被调用。
// 可停止观察 observer.disconnect();observe()
配置 MutationObserver 在 DOM 更改匹配给定选项时,通过其回调函数开始接收通知。
// 以上述配置开始观察目标节点 observer.observe(targetNode, config)takeRecords()
从 MutationObserver 的通知队列中删除所有待处理的通知,并将它们返回到MutationRecord对象的新Array中。
const mutations = observer.takeRecords();
2. MutationObserver一般用来做什么?
(1) 防止第三方注入js文件
(2) 防止删除前端生成的水印
export const observerDOM = (targetNode, vm) => {
// const targetNode = document.querySelector(target);
// 观察器的配置(需要观察什么变动)
const config = {attributes: true, childList: true, subtree: true};
// 当观察到变动时执行的回调函数
const callback = function (mutationsList, observer) {
// Use traditional 'for loops' for IE 11
console.log("mutationsList", mutationsList)
for (let mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.removedNodes.length > 0) {
if (mutation.removedNodes[0].id === "1.23452384164.123412415" ||
mutation.removedNodes[0].id === "1.23452384164.123412422" ||
mutation.removedNodes[0].id === "1.23452384164.123412322"
) {
console.log("mutation.removedNodes[0].id", mutation.removedNodes[0].id)
vm.$baseMessage("请不要尝试删除水印", 'error');
location.reload();
// console.log('A child node has been added or removed.');
}
} else if (mutation.type === 'attributes') {
if (mutation.target.id === "1.23452384164.123412415" ||
mutation.target.id === "1.23452384164.123412422" ||
mutation.target.id === "1.23452384164.123412322") {
vm.$baseMessage("请不要尝试删除水印", 'error');
location.reload();
}
}
}
};
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
// 以上述配置开始观察目标节点
observer.observe(targetNode, config)
}
// 函数调用
observerDOM(this.$refs.tableContainer, this)
(3) 用来处理页面的敏感数据
主要我们说一下如何使用这个API来实现前端处理敏感数据,因为我们有个项目是为第三方系统处理敏感数据的,需要导入我们自己的工具js来实现前端处理敏感数据。经过调研发现这个API应该可以处理。只要我监听页面的每个DOM元素,获取到DOM元素的值就能实现这个功能了!想法很饱满,现实很骨感。
最先我们使用innerText来获取DOM元素的值,获取到的值发现既包含文本节点的值又包含了子DOM节点的值,所以一直替换失败。所以在写代码前先搞清楚几个概念。
-
DOM的最小组成单位叫做节点(node),一个文档的树形结构(DOM树),就是由各种不同类型的节点组成。
-
对于HTML文档,节点主要有以下六种类型:Document节点、DocumentType节点、Element节点、Attribute节点、Text节点和DocumentFragment节点。
-
六种类型分别对应的nodeType
类型 nodeName nodeType DOCUMENT_NODE #document 9 ELEMENT_NODE 大写的HTML元素名 1 ATTRIBUTE_NODE 等同于Attr.name 2 TEXT_NODE #text 3 DOCUMENT_FRAGMENT_NODE #document-fragment 11 DOCUMENT_TYPE_NODE 等同于DocumentType.name 10
经过重新温习基础的知识,仔细想一下,我们只是获取文本节点的值用来进行处理。所以对应就是target.firstChild.nodeValue。
处理的伪代码如下:
const filterTags = ['BODY', 'SCRIPT', 'HTML', 'META', 'TITLE', 'HEAD', 'SCRIPT', 'TABLE', 'IMG', 'H1', 'LINK', 'NOSCRIPT', 'SVG', 'VIDEO'];
const valueTags = ['INPUT', 'TEXTAREA'];
const options = {
childList: true,
subtree: true,
attribute: true,
characterData: true,
attributeFilter: ['class', 'style']
};
const observer = new MutationObserver(mutations => {
for(let mutation of mutations) {
if (!filterTags.includes(mutation.target.nodeName)) {
if (mutation.target instanceof HTMLElement) {
if (valueTags.includes(mutation.target.nodeName)) {
// 处理input,textarea中的值
// 处理逻辑
} else {
if (mutation.target.firstChild.nodeValue) {
// 这里根据DOM的节点来获取值
// 得到值来处理
} else {
if (Array.form(mutation.target.children).length > 0) {
flatDomNodes(mutation.target.firstChild.nodeValue, Array.form(mutation.target.children))
}
}
}
}
}
}
});
const domElem = document.documentElement
observer.observe(domElem, options);
function flatDomNodes(parentText, arr = []) {
if (!Array.isArray(arr)) return;
for (let i = 0; i < arr.length; i++) {
if (arr[i].firstChild.nodeValue && (parentText !== arr[i].firstChild.nodeValue)) {
// 处理值的逻辑 arr[i].firstChild.nodeValue
} else if (valueTags.includes(arr[i].nodeName)) {
// 处理值的逻辑 arr[i].value
}
if (Array.form(arr[i].children).length > 0) {
flatDomNodes(arr[i].firstChild.nodeValue, Array.form(arr[i].children))
}
}
}
经过测试发现我们的第一版本的代码经常会导致页面卡死,通过排查发现我们这段递归的代码导致,因为页面的嵌套太深了,遇到几百条的表格那种直接卡死,甚至有时候浏览器就直接崩溃了。还有就是在递归中添加了setTimeout定时器。直呼好家伙,这不卡死才怪。
这时候突然想到之前看到的一个API,document.createNodeIterator
!MDN NodeIterator 接口表示一个遍历 DOM 子树中节点列表的成员的迭代器。节点将按照文档顺序返回。
const nodeIterator = document.createNodeIterator(root[, whatToShow[, filter]]);
仔细想一下这不正是我们递归想要实现的地方么?所以把我们的代码改一下,去掉递归。
const filterTags = ['BODY', 'SCRIPT', 'HTML', 'META', 'TITLE', 'HEAD', 'SCRIPT', 'TABLE', 'IMG', 'H1', 'LINK', 'NOSCRIPT', 'SVG', 'VIDEO'];
const valueTags = ['INPUT', 'TEXTAREA'];
const options = {
childList: true,
subtree: true,
attribute: true,
characterData: true,
attributeFilter: ['class', 'style']
};
const observer = new MutationObserver(mutations => {
for(let mutation of mutations) {
if (!filterTags.includes(mutation.target.nodeName)) {
if (mutation.target instanceof HTMLElement) {
if (valueTags.includes(mutation.target.nodeName)) {
// 处理input,textarea中的值
// 处理逻辑
} else {
getChildren(mutation.target);
}
}
}
}
}
});
const domElem = document.documentElement
observer.observe(domElem, options);
// function flatDomNodes(parentText, arr = []) {
// if (!Array.isArray(arr)) return;
// for (let i = 0; i < arr.length; i++) {
// if (arr[i].firstChild.nodeValue && (parentText !== arr[i].firstChild.nodeValue)) // {
// 处理值的逻辑 arr[i].firstChild.nodeValue
// } else if (valueTags.includes(arr[i].nodeName)) {
// 处理值的逻辑 arr[i].value
// }
// if (Array.form(arr[i].children).length > 0) {
// flatDomNodes(arr[i].firstChild.nodeValue, Array.form(arr[i].children))
// }
// }
// }
function getChildren(parent) {
let t = document.createNodeIterator(parent, NodeFilter.SHOW_ALL),{
acceptNode(node) {
// 处理的逻辑
}
});
let currNode = null;
while((currNode = t.nextNode()) !== null) {
// 处理逻辑
}
}
在处理过程中可能会遇到执行顺序的问题,一定要注意代码的执行时机。还有一个比较坑的地方:js相同的正则多次调用test()返回的值却不同的问题。
这是因为正则reg的g属性,设置的全局匹配。RegExp有一个lastIndex属性,来保存索引开始位置。
我们的解决方案:每次匹配之前将lastIndex的值设置为0。