Dom-apiMutationObserver使用方法详解

2022-11-08 21:08:20
目录
1. 概述2. 基本使用2.1 observer 方法2.2 MutationObserverInit 对象2.3 disconnect()方法2.4 takeRecords3. MutationRecord3.1 MutationRecord 实例4. MutationObserver 实战总结

1.>

MutationObserver 接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。 - MDN

也就是说,当监视的 DOM 发生变动时 MutationObserver 将收到通知并触发事先设定好的回调函数。这个功能非常强大,意味着对于我们可以更加方便的动态操作 DOM 元素了。

你是否能联想到某些业务场景呢?

像这样的列表页,由于文案和文章配图数量的不同导致有多种不同的 ui 设计和排列方式,所以在前端对数据渲染的时候,要对列表每一项内容类型进行甄别。使用 MutationObserver 可以非常简单的完成这个需求

2.>

MutationObserver 是一个构造函数,通过调用 MutationObserver 构造函数并传入一个回调函数来创建一个观察 DOM 的实例

const observer = new MutationObserver(() => console.log('DOM 发生变化了~'));

回调参数的两个参数:

    mutationRecords:数组队列,记录操作的结果observer:与构造函数的返回值 全等,因为这个回调函数是 异步执行,所以也可以访问到外部的 observer

    后文还会再详细讨论这两个参数

    2.1>

    新创建的 MutationObserver 实例不会关联 DOM 的任何部分。要把这个 observerDOM 关联起来,需要使用 observe()方法

    observer.observe(document.body, { attributes: true });
    

    这个方法接收必需的参数:

      第一个参数:要观察的 DOM 节点第二个参数:MutationObserverInit 对象

      这样 document.body 就被观察了,只要 document.body 元素的任何属性值发生变化,就会触发观察对象,并且 异步调用 传入 MutationObserver 的回调函数(这是一个 微任务)

      const observer = new MutationObserver(() => console.log('DOM 发生变化了~'));
      observer.observe(document.body, { attributes: true });
      setTimeout(() => {
        document.body.className = 'test';
      }, 1000);
      

      等过了一秒之后,定时器的回调函数执行,修改了 document.bodyclass 属性,所以了触发 MutationObserver 的回调函数

      2.2>

      在上面的例子中,只要 document.body 本身的任意属性发生了,都会被观察到,但是其他修改 DOM 的行为不会被观察,例如节点的增删改查,子节点属性的修改...,因为我们在调用 observe() 方法的时候传入的 MutationObserverInit 对象添加了 attributes 属性,所以 observe() 方法作用是只能侦测自身的元素属性值的变化。MutationObserverInit 对象除了这个属性之外,还有很多非常强大的属性可以观察更多的节点操作

      MutationObserverInit 对象用于控制对目标节点的观察范围。观察方式的类型有 属性变化、文本变化 和 子节点变化 这三种。

      所以在调用 observe()时,MutationObserverInit 对象中的 attribute(属性变化)、characterData(文本变化) 和 childList(子节点变化) 属性必须 至少有一项 为 true(无论是直接设置这几个属性,还是通过设置 attributeOldValue(属性变化)等属性间接导致它们的值转换为 true)。否则会抛出错误,因为 DOM 的变化不会被任何变化事件类型触发回调。

        属性变化

        观察节点 属性 的 添加、移除 和 修改。需要在 MutationObserverInit 对象中将 attributes 属性设置为 true

        const observer = new MutationObserver(() => console.log('DOM 发生变化了~'));
        observer.observe(document.body, { attributes: true });
        setTimeout(() => {
          document.body.className = 'test';
        }, 1000);
        

        还有 attributeOldValue: true:可以记录变化之前的属性值。
        attributeFilter: ['class', 'id']:可以观察哪些属性的变化,在这里只观察了 classid 属性

          文本变化

          观察文本节点(如 Text 文本节点、Comment 注释 ) 中字符的 添加、删除 和 修改。要在 MutationObserverInit 对象中将 characterData 属性设置为 true

          const observer = new MutationObserver(() => console.log('DOM 发生变化了~'));
          observer.observe(document.body.firstChild, { characterData: true });
          setTimeout(() => {
            document.body.firstChild.textContent = '123';
          }, 1000);
          

          还有 characterDataOldValue:可以记录变化之前的文本值

            观察子节点

            观察目标节点子节点的添加和移除。需要在 MutationObserverInit 对象中将 childList 属性设置为 true

            const observer = new MutationObserver(() => console.log('DOM 发生变化了~'));
            observer.observe(document.body, { childList: true });
            setTimeout(() => {
              document.body.appendChild(document.createElement('div'));
            }, 1000);
            

            在这个例子中控制台输出两次,第一次是 body 元素在 0s 触发回调,第二次才是新创建的元素在 1s 之后触发回调,因为观察 document.body 会在创建 body 的时候就立即被观察到,而观察非 body 元素,不会触发自身创建的过程

            childList 只会观察子节点,但不会观察深层的节点,可以在 MutationObserverInit 对象中将 subtree 属性设置为 true,还得将 childListtrue,因为 MutationObserverInit 对象中的 attributecharacterDatachildList 属性必须 至少有一项 为 true

            <div></div>
            <script>
              const observer = new MutationObserver(mutationRecords => {
                console.log('触发了');
                console.log(mutationRecords.length); // 2
              });
              observer.observe(document.body.children[0], { childList: true, subtree: true });
              setTimeout(() => {
                document.body.children[0].appendChild(document.createElement('div'));
                document.body.children[0].children[0].appendChild(document.createElement('div'));
              }, 1000);
            </script>
            

            这里虽然只会触发一次回调,但是会在 mutationRecords 这个数组中会分别记下两次 DOM 操作的记录,所以数组的长度为 2

            <div></div>
            <script>
              const observer = new MutationObserver(mutationRecords => {
                console.log('触发了');
                console.log(mutationRecords.length); // 1
              });
              observer.observe(document.body.children[0], { childList: true, subtree: true });
              setTimeout(() => {
                document.body.children[0].appendChild(document.createElement('div'));
              }, 1000);
              setTimeout(() => {
                document.body.children[0].children[0].appendChild(document.createElement('div'));
              }, 1000);
            </script>
            

            这个例子与上个例子区别是将两次 DOM 操作放在两个不同的定时器执行,但是结果却是截然不同,这里会输出两次,mutationRecords 数组的长度为 1

            这是因为 DOM 操作是同步的,DOM 渲染是异步的,MutationObserver 中的回调函数执行会被包裹在一个 微任务 中,而定时器是 宏任务,所以整个执行过程是:第一个定时器先执行,观察 DOM 的回调函数执行,第二个定时器再执行,所以 DOM 变化被观察了两次。

            上一个的例子 DOM 操作是在同一个 宏任务 中执行,因为浏览器会优化 DOM 渲染的过程,所以等到两个 div 元素创建完毕才会渲染,之后执行观察 DOM 的 微任务,所以才会触发一次观察,但是产生了两个结果,所以 mutationRecords 数组的长度为 2

            这里还有一个怪异现象,在第二个例子中,为什么两输出 mutationRecords 的长度都是 1,因为这两个数组不是同一个数组,关于为什么 mutationRecords 数组 不会缓存 第一次的操作结果,而是创建两个不同的数组,会在后面的内容详细讨论。

            2.3>

            默认情况下,只要被观察的元素不被垃圾回收,MutationObserver 的回调就会响应 DOM 变化事件,从而被执行。想要 提前终止执行 回调,可以调用 disconnect() 方法。

            <div></div>
            <script>
              const observer = new MutationObserver(mutationRecords => {
                console.log('触发了');
              });
              observer.observe(document.body.children[0], { childList: true, subtree: true });
              setTimeout(() => {
                document.body.children[0].appendChild(document.createElement('div'));
                setTimeout(() => {
                  observer.disconnect();
                }, 0);
              }, 1000);
              setTimeout(() => {
                document.body.children[0].appendChild(document.createElement('div'));
              }, 2000);
            </script>
            

            在这个例子中,在第一秒的时候执行了 DOM 操作,并且创建一个定时器包裹 disconnect() 方法,然后执行 disconnect() 方法,在第二秒的时候执行了另外一个 DOM 操作。所以结果只有第一次 DOM 操作会被观察到

            为什么这里需要将 disconnect 方法计时器里执行呢,千万别忘了,DOM 操作是 同步执行 的,DOM 渲染是 异步执行 的,disconnect() 也是 同步执行 的。如果不添加定时器,在 DOM 渲染值之前就取消了观察,虽然操作了 DOM,但是渲染过程并没有观察到

            2.4>

            调用 MutationObserver 实例的 takeRecords() 方法可以清空记录队列,取出并返回其中的所有 MutationRecord 实例。

            const observer = new MutationObserver(mutationRecords => {
              console.log(mutationRecords); // 不输出
            });
            observer.observe(document.body, { attributes: true });
            document.body.className = 'test1';
            document.body.className = 'test2';
            document.body.className = 'test3';
            console.log(observer.takeRecords().length); // 3
            console.log(observer.takeRecords().length); // 0
            

            在这个例子中,操作了 3DOM,所以在调用第一次 takeRecords() 方法的时候会输出 3,并且切断了与观察对象的联系,所以不会触发 MutationObserver 的回调,但是这种切断关系是 不牢靠 的,也就意味着下次的 DOM 操作会 重启观察,就像下面的这个例子表现的一样

            const observer = new MutationObserver(mutationRecords => {
              console.log(mutationRecords); // 输出两次
            });
            observer.observe(document.body.children[0], { attributes: true });
            document.body.children[0].className = 'test1';
            document.body.children[0].className = 'test2';
            document.body.children[0].className = 'test3';
            observer.takeRecords();
            document.body.children[0].className = 'test4';
            setTimeout(() => {
              document.body.children[0].className = 'test5';
            });
            

            3.>

            MutationRecord 是一个 记录队列 的数组,,仅当 微任务队列 没有其他的微任务回调时(队列中微任务 长度为 0),才会将观察者注册的 回调 作为微任务放置到任务队列上。这样可以保证记录队列的内容不会被回调处理两次。

            在回调的微任务异步执行期间,有可能又会发生更多变化事件。因此被调用的回调会接收到一个 MutationRecord 实例的数组,顺序为它们进入记录队列的顺序。回调要负责处理这个数组的每一个实例,因为 回调函数 退出之后这些实现就不存在了。回调函数执行完成后,这些 MutationRecord 就用不着了, 因此记录队列会被清空,其内容会被丢弃。所以每一个回调函数中的 MutationRecords 数组是 不同的实例

            3.1>
            const observer = new MutationObserver(mutationRecords => {
              console.log(mutationRecords);
            });
            const oDiv = document.getElementsByTagName('div')[0];
            observer.observe(oDiv, { attributeOldValue: true });
            oDiv.classList.add('box');
            

            几个重要的属性:

            属性说明
            target被修改影响的目标节点
            type表示变化的类型:"attributes"、"characterData"或"childList"
            oldValue如果在 MutationObserverInit 对象中启用(attributeOldValue 或 characterData OldValue 为 true),"attributes"或"characterData&quot;的变化事件会设置这个属性为被替代的值 "childList"类型的变化始终将这个属性设置为 null
            addedNodes对于"childList"类型的变化,返回包含变化中添加节点的 NodeList 默认为空 NodeList

            4.>

            一个简单的业务场景:

            用户提交评论,如果评论的内容超过最大宽度,需要隐藏多余的部分,同时展示“查看更多”按钮,点击这个按钮就会展示评论的全部内容

            难点:只有当 DOM 被渲染的时候 才知道实际的高度,所以无法预先分析评论文本内容而选择渲染方式的类型

            实现思路:使用 MutationObserver 监听评论区列表,每当用户提交新的评论,新生成的 DOM 就会被观察到,判断评论的内容是否超出最大高度,更新 UI

            <script lang="ts" setup>
            import { onMounted, reactive, ref } from 'vue';
            interface ICommentItem {
              id: string;
              text: string;
              showBtn: boolean;
            }
            const comIptVal = ref('');
            const commentList = reactive<ICommentItem[]>([]);
            const commentListRef = ref<HTMLElement | null>(null);
            const MaxSize = 50; // 每一项最大高度
            const observer = new MutationObserver(mutationRecord => {
              const currRecord = mutationRecord[mutationRecord.length - 1]; // 最新的记录
              const newNode = currRecord.addedNodes[currRecord.addedNodes.length - 1] as HTMLElement; // 新添加的节点
              // 新增加的按钮也会触发观察,所以要判断新增加节点是否是评论  
              if (newNode.className === 'comment-item') {
                const id = newNode.dataset.id;
                const item = commentList.find(item => item.id === id)!;
                if (newNode.clientHeight > MaxSize) {
                  // 如果超出最大高度
                  const oText = newNode.children[0] as HTMLElement;
                  oText.style.height = MaxSize + 'px';
                  oText.style.overflow = 'hidden';
                  item.showBtn = true;
                }
              }
            });
            onMounted(() => {
              observer.observe(commentListRef.value as HTMLElement, {
                subtree: true,
                childList: true,
              });
            });
            const addCommentItem = () => {
              commentList.push({
                id: String(new Date().getTime()), // 评论的 id
                text: parseComment(comIptVal.value), // 解析输入文本内容
                showBtn: false, // 默认不超出最大高度
              });
            };
            const parseComment = (str: string) => {
              return str.replace(/[\n\r]/g, '<br />'); // 将 \n 换行解析成 <br /> 元素
            };
            const showAllBtnClick = (el: HTMLElement, item: ICommentItem) => {
              el.style.overflow = 'visible';
              el.style.height = 'auto';
              item.showBtn = false; // 隐藏点击更多按钮
            };
            const child = reactive<HTMLElement[]>([]); // 循环绑定 DOM
            </script>
            <template>
              <textarea v-model="comIptVal"></textarea>
              <button @click="addCommentItem">添加</button>
              <ul class="comment-list" ref="commentListRef">
                <li class="comment-item" v-for="(item, index) in commentList" :key="item.id" :data-id="item.id">
                  <div v-html="item.text" :ref="(el: any) => child[index] = el"></div>
                  <button v-if="item.showBtn" @click="showAllBtnClick(child[index] as HTMLElement, item)">
                    更多
                  </button>
                </li>
              </ul>
            </template>
            

            参考文献

            JavaScript 高级程序设计第 4 版.PDF – 1024.Cool

            总结

            MutationObserver>DOM 元素的变化,在传入构造函数的回调函数中可以访问到触发 DOM 变化的 target 和 影响 DOM 变化的结果

            以上就是Dom-api MutationObserver使用方法详解的详细内容,更多关于Dom-api MutationObserver方法的资料请关注易采站长站其它相关文章!