前言
打开自己的博客,猛然发现自己已经有快两个月没有更新了,一方面是因为确实没有遇到什么特别值得记录的问题及知识点,另一方面则是所在部门的组织架构调整,唉(😔)日子难过啊,没啥心情更新。
正题
众所周知,我们监听input值得变化,我们可以通过绑定事件来实现。但是对于DOM元素的属性、数据的变化我们应该如何监听呢。
这个时候就需要用到我们今天的主角了:MutationObserver,它提供了监视对DOM树所做更改的能力。目前可以监听DOM节点的属性、CharacterData以及子元素的变化。
介绍
首先,我们需要明确一点那就是MutationObserver是一个构造函数。它接受一个回调函数作为参数,回调函数在监听到目标DOM变化时会执行。
const mutation = new MutationObserver(callback);
当构建出对象之后我们需要确定我们要用这个对象监听哪个DOM,这个是通过提供的方法observe()来进行绑定的。这个方法接受两个参数,第一个参数就是目标DOM,第二个参数就是一个配置对象config,用来配置我们要对什么属性进行监听。
config的配置属性:
- attributes 值为布尔值,配置是否监听DOM的属性变化
- attributeFilter 值为一个数组,配置应该监听那些属性变化。
- attributeOldValue 值为布尔值,如果为true,那么回调函数的第一个参数对象中将多一个属性oldValue用来记录上一次的属性值。
- characterData 值为布尔值,配置是否监听元素内文本内容的变化(这个说法可能不太准确,或者TextNode内的文本会更准确)。
- characterDataOldValue 值为布尔值,功能同attributeOldValue。
- childList 值为布尔值,配置是否监听子元素的数量变化。
- subtree 值为布尔值,配置是否对所有后代元素也执行监听。
const config = {
attributes: true
}
mutation.observe(document.getElementById("id"), config);
既然我们可以监听DOM变化,那我们必然也可以解除监听。我们可以通过disconnect()来实现,这个没啥好说的,直接调用就可以了。
mutation.disconnect();
还有一个我认为不常用的方法takeRecords(),这个方法是用来获取已经捕获到的变化,但是还没有执行对应的回调的DOM更改列表。我的理解是当属性快速变化时,MutationObserver会维护一个callback栈,一个个出栈执行,当断开监听时,有可能会有未执行的回调,此时需要对这些回调执行特殊的处理。
支持该功能的浏览器版本:
基本使用
我直接上一段我在React中使用的代码吧。
import { Button } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
const MutationDOM = () => {
const [domInfo, setInfo] = useState('');
const [styleFlag, setStyle] = useState(false);
const mutationRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
let mutation:MutationObserver;
if(mutationRef.current) {
const callback = (mutations: MutationRecord[], observer: MutationObserver) => {
setInfo(JSON.stringify(mutations) + '-------' + JSON.stringify(observer));
console.log(mutations, observer);
}
const config = {
attributes: true,
// attributeFilter: ['style', 'childList'],
attributeOldValue: true,
characterData: true, // 监听元素内的数据变动
characterDataOldValue: true,
childList: true, // 监听子元素数量变动(子元素的属性变化不会触发事件)(只针对一级子元素,孙子元素的数量变化不会触发事件)
subtree: false, // 将observer事件下发到目标元素的所有子元素,相当于对子元素也进行了observer
}
mutation = new MutationObserver(callback);
// cofig设置项必须的有,否则的话会报错(The options object must set at least one of 'attributes', 'characterData', or 'childList' to true)
mutation.observe(mutationRef.current, config);
}
if(inputRef.current) {
const config = {
attributes: true,
// attributeFilter: ['style', 'childList'],
attributeOldValue: false,
characterData: true, // 监听元素内的数据变动,准确的说是监听target下的textNode的变化
characterDataOldValue: true,
childList: true, // 监听子元素数量变动(子元素的属性变化不会触发事件)(只针对一级子元素,孙子元素的数量变化不会触发事件)
subtree: true, // 将observer事件下发到目标元素的所有后代(不是子元素)元素,相当于对后代元素也进行了observer
}
mutation.observe(inputRef.current, config);
}
mutation.disconnect();
return () => {
if(mutation) {
mutation.disconnect();
}
}
}, [])
return (
<>
<div style={{textAlign: 'center'}}>
<Button onClick={() => setStyle(!styleFlag)}>改变节点属性</Button>
</div>
{/* resize属性可以使元素可调节宽高,但是对于行内元素及设置了overflow: visible;属性的元素不会生效 */}
<div ref={mutationRef} style={{width: 500, height: 500, margin: '0 auto', resize: 'both',overflow: 'auto', background: '#ACF233'}}>
<div style={{width: 200, height: 200, overflow: 'auto', background: 'red', resize: 'both', margin: '0 auto'}}>
<div style={{width: 100, height: 100, overflow: 'auto', background: '#AAA', resize: 'both', margin: '0 auto'}}></div>
</div>
<div style={{border: styleFlag ? '1px solid #AAA' : '2px solid #000'}}></div>
</div>
<input ref={inputRef} style={{color: styleFlag ? 'red' : 'blue'}} />
<div style={{textAlign: 'center'}}>
{domInfo}
</div>
</>
)
}
export default MutationDOM;
对于characterData的监听
这个单独挑出来说,是因为它比较特殊,是对textNode的监听。上代码可能比较好理解
import { Button } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
const MutationDOM = () => {
const [domInfo, setInfo] = useState('');
const charactorRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let mutation:MutationObserver;
if(charactorRef.current && charactorRef.current.childNodes[0]) {
const callback = (mutations: MutationRecord[], observer: MutationObserver) => {
console.log(mutations, observer);
}
const config = {
attributes: false,
attributeOldValue: false,
characterData: true,
characterDataOldValue: true,
childList: false,
subtree: false,
}
mutation = new MutationObserver(callback);
mutation.observe(charactorRef.current.childNodes[0], config);
}
return () => {
if(mutation) {
mutation.disconnect();
}
}
}, [])
return (
<>
<div ref={charactorRef} contentEditable style={{border: '1px solid #AAA'}}>111</div>
</>
)
}
export default MutationDOM;
需要格外注意contentEditable属性,这个属性可以使元素可编辑。
注意点
这是我在自己使用时总结的一些需要注意的点
- 一个MutationObserver对象可以对多个DOM进行监听,不需要多次实例化对象。除非callback有变化。
- childList属性是监听子元素的删除和增加,也就是说只对子元素的数量变化进行响应,子元素的属性变化不会监听。同样,孙子元素的变化也不会被监听到。
- 当你调用observe()方法时config是必填的,而且attributes,characterData,childList必须有一个属性设置为true,否则会报错。
结语
这些都是自己查资料以及实践学习总结出来的一些东西,希望能帮助各位加深印象,如果有不对的地方还请指正。