5

视觉走查插件开发之深入MutationObserver

 2 years ago
source link: https://jelly.jd.com/article/614c4ae6b5b99e018e8893e3
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
JELLY | 视觉走查插件开发之深入MutationObserver
视觉走查插件开发之深入MutationObserver
上传日期:2021.09.28
Flare是团队最近打造的一款视觉走查产品,一期PC端解决方案是借助于Chrome扩展插件,本文主要介绍MutationObserver在其中的深入使用经验。

最近团队在打造一款·视觉走查·产品,目的是提效人工核对走查流程,可视化 查看视觉走查问题云协作,数字化展示视觉走查问题,提供走查问题看板,实现全新工作流协作形式,为设计师测试人员研发工程师三者在视觉走查流程工作中提供便利、高效、智能的协同走查工具。一期PC场景载体是 Chrome 扩展插件,体验地址请 猛击

这款产品其中的一个需求点是这样的:在视觉走查过程中,当鼠标点击一个 HTML DOM 节点的时候,根据节点样式的信息,与视觉稿进行比对,如果有不一致的情况,提报一个问题,并在该节点元素上打一个气泡点,代表这个节点有问题,需要前端同学去修复。如果这个 DOM 节点通过用户的某些操作(鼠标 clickhover)才触发可见,例如 Modal 弹框Popover 气泡下拉菜单display:none 等,就没办法在初始化页面的时候,就把这个气泡点渲染在 DOM 里,这时候就需要监听 DOM 的变化,下面就使用到了今天的主角-MutationObserver

API 介绍

先来看下 MDNMutationObserver的介绍

MutationObserver 给开发者们提供了一种能在某个范围内的 DOM 树发生变化时作出适当反应的能力。该 API 设计用来替换掉在 DOM 3 事件规范中引入的 Mutation 事件

Mutation Observer 是在 DOM level 4 中定义的新 API,可以监听 DOM 的变化,单词 mutation“突变”的意思,observer“观察者”的意思,连起来就是“突变观察者”的意思。该 API 执行逻辑是先观察,再执行,是一个异步的过程。

简单概述一下:

  1. 监视 DOM 变动的接口 当监视的 DOM 发生变动时 MutationObserver 将收到通知并触发事先设定好的回调函数。

  2. 类似于事件,但是异步触发 添加监视时,MutationObserver 上的 observer函数与 addEventListener 有相似之处,但不同于后者的同步触发,MutationObserver 是异步触发,此举是为了避免 DOM 频繁变动导致回调函数被频繁调用,造成浏览器卡顿。

该构造函数用于实例化一个新的 MutaionObserver ,同时指定触发 DOM 变动时的回调函数:

var observer = new MutationObserver(callback);
    function callback(mutationList, observer){}

callback,即回调函数接收两个参数,第一个参数是一个包含了所有 MutationRecord 对象的数组,第二个参数则是这个MutationObserver 实例本身。

  1. observe(target[, options])

target:DOM 树中的一个要观察变化的 DOM Node (可能是一个 Element) , 或者是被观察的子节点树的根节点。 options: 可选 ,一个可选的 MutationObserverInit 对象,此对象的配置项描述了 DOM 的哪些变化应该提供给当前观察者的 callback。

以下是 MutationObserverInit 对象的各属性及其描述:

属性类型描述childListBoolean是否观察子节点的变动attributesBoolean是否观察属性的变动characterDataBoolean是否节点内容或节点文本的变动subtreeBoolean是否观察所有后代节点的变动attributeOldValueBoolean观察 attributes 变动时,是否记录变动前的属性值characterDataOldValueBoolean观察 characterData 变动时,是否记录变动前的属性值chilattributeFilterdListBoolean表示需要观察的特定属性(比如['class','src']),不在此数组中的属性变化时将被忽略

注:不能单独观察 subtree 变动,必须同时指定 childList、attributes 和 characterData 中的一种或多种。 为同一个 DOM 节点多次添加同一个 MutationObserver 是无效的,回调函数将只被触发一次。但如果指定不同的 options 对象(即观察不同的变动),即被视为不同的 MutationObserver。

  1. disconnect()

该方法用来停止观察。后续如果 DOM 节点发生变动将不再触发回调函数。

    observer.disconnect();

disconnect 函数和 removeEventListener大致类似,除了 disconnect 函数比较粗暴(无法传参配置),会把该 MutationObserver 实例上所有的观察都停止。

  1. takeRecords()
    mutationRecords = observer.takeRecords()

返回值:返回一个MutationRecord 对象列表,每个对象都描述了应用于DOM树某部分的一次改动。

注:调用takeRecords()后,已发生但未传递给回调的变更队列将保留为空

针对需求背景的解决方案

import * as React from "react";

const MutationTags: React.FC = () => {
  const [, setTime] = React.useState(Date.now());
  const forceUpdate = () => {
    setTime(Date.now());
  };

  // 当观察到变动时执行的回调函数
  const callback = function (mutationsList, observer) {
    forceUpdate();
  };

  // 创建一个观察器实例并传入回调函数
  const observer = new MutationObserver(callback);

  React.useEffect(() => {
    // 选择需要观察变动的节点
    const targetNode = document.body;
    // 观察器的配置(需要观察什么变动)
    const config = { attributes: true, childList: true, subtree: true };
    // 开始观察目标节点
    observer.observe(targetNode, config);
    return () => {
      // 之后,可停止观察
      observer.disconnect();
    };
  }, []);

  return <div>这里是气泡</div>;
};

export default MutationTags;

在鼠标点击下拉菜单时,下拉菜单 display:block,观察器实例 observer 收到通知并触发回调函数 callback,在 callback 里强制触发 rerender,气泡点自动渲染在有问题的 DOM 节点上

11d36b920f7b79f2.png

MutationObserver 模式的优点

相比DOM 3Mutation eventsMutationObserver性能要更高,在EventLoop 中属于 微任务(micro task)。Mutation Events 是同步执行的,它的每次调用,都需要从事件队列中取出事件,执行,然后事件队列中移除,期间需要移动队列元素。如果事件触发的较为频繁的话,每一次都需要执行上面的这些步骤,那么浏览器会被拖慢。而 MutationObserver 所有监听操作以及相应处理都是在其他脚本执行完成之后异步执行的,并且是所以变动触发之后,将变得记录在数组中,统一进行回调的,也就是说,当你使用 observer 监听多个 DOM 变化时,并且这若干个 DOM 发生了变化,那么 observer 会将变化记录到变化数组中,等待一起都结束了,然后一次性的从变化数组中执行其对应的回调函数。

扩展使用场景

  1. 监听 SPA 网页路由切换

对于使用 React-Router 或者 Vue-Router 开发的 SPA,可以通过监听 hashchangepopstate 来实现,但是 popstate只能监听用户触发的浏览器的前进和后退,对于 history.push 或者 history.replace,是不能触发监听的。 一个解决办法是重写 historypushStatereplaceState。但是,对于 Chrome 浏览器插件,插件注入的代码和网页本身的代码之前是不共享作用域的,也就是说,插件代码里对于 history 的重写,对网页本身的代码不起作用,这个方案不适用。 另一个解决办法是使用 MutationObserver 监听 dom 的变化, 路由切换,dom 肯定会发生变化,获取 url与之前的url对比,如果不相等,就一定代表路由发生了切换

想象一下,你需要添加一个第三方脚本,该脚本不仅包含有用的功能,还会执行一些我们不想要的操作,例如显示广告 <div class="ad">这里是广告</div>

使用 MutationObserver,我们可以监测到我们不需要的元素何时出现在我们的 DOM 中,并将其删除。

  1. 动态跟踪高亮元素

在web编辑器中动态加载代码片段,有了MutationObserver,它可以随时监听插入,高亮最新的代码片段

11d36b920f7b79f2.png

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK