使用Mutation Observer监听DOM变化(也许是最完全指南?)

前言

打开自己的博客,猛然发现自己已经有快两个月没有更新了,一方面是因为确实没有遇到什么特别值得记录的问题及知识点,另一方面则是所在部门的组织架构调整,唉(😔)日子难过啊,没啥心情更新。

正题

众所周知,我们监听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栈,一个个出栈执行,当断开监听时,有可能会有未执行的回调,此时需要对这些回调执行特殊的处理。

支持该功能的浏览器版本:
支持MutationObserver的浏览器版本

基本使用

我直接上一段我在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,否则会报错。

结语

这些都是自己查资料以及实践学习总结出来的一些东西,希望能帮助各位加深印象,如果有不对的地方还请指正。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值