浏览器中好用的 Observer Api 你知道多少?

浏览器中好用的 Observer Api

简介

网页开发中经常会处理用户交互相关的事件。随着我们的需求越来越复杂,有很多的场景浏览的事件机制不能很好,或者说不能很快速的实现我们的需求。比如:

  • 页面水印需要防止水印元素被篡改。

  • 在实现图片懒加载的时候,我们需要判断元素是否出现在了可视区域内。

  • 在一些复杂的交互中,我们需要判断元素的大小是否发生了变化。

目前浏览器提供了 5 种很好用的 Observer Api,可以很方便的实现上面的需求。

Observer Api 属于微任务 ,优先级小于 Promise,每一个Observer 在创建的时候会调用一次,然后每次监听的相关事件触发的时候会执行回调。

接下来我会对这几个 API 的使用、兼容性、polyfill 做一些介绍。

IntersectionObserver

相交观察者,可以很方便的检测一个元素是否可见或者两个元素是否相交

兼容性

polyfill

具体见链接readme说明,官方的polyfillgood

小案例

图片懒加载案例,核心逻辑是将src使用data-src进行替换,当图片进入视口的时候赋值src属性。

代码

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>IntersectionObserver</title>
    <style>
      .content {
        width: 200px;
        height: 1000px;
        border: 1px solid red;
      }
    </style>
  </head>
  <body>
    <div class="content">文字内容</div>
    <img data-src="./img/1.png" width="1000" />
    <div class="content">文字内容</div>
    <img data-src="./img/2.jpg" width="1000" />
    <div class="content">文字内容</div>
    <img data-src="./img/3.jpg" width="1000" />

    <script>
      const imgs = document.querySelectorAll('img');
      imgs.forEach((img) => {
        // 1. 创建观察者
        const observer = new IntersectionObserver((entry) => {
          if (entry[0].isIntersecting) {
            // 图片出现在视口区域了,加载图片
            img.src = img.getAttribute('data-src');
            img.removeAttribute('data-src');
            observer.unobserve(img); // 3. 取消监听
          }
        });
        observer.observe(img); // 2. 开始监听
      });
    </script>
  </body>
</html>

预览

详细使用

IntersectionObserver API 是一个相交观察器,用于监听 目标元素 与指定的 root 元素(祖先元素或视窗) 的相交状态(可见性)。

  • 目标元素进入 root 元素的时候会触发回调
  • 目标元素离开 root 元素的时候会触发回调

构造函数

js 复制代码
const observer = new IntersectionObserver(callback, options);

options

js 复制代码
let options = {
  root: document.querySelector('#app'),
  rootMargin: '0px',
  threshold: 1.0,
};
  • root:指定 root 元素,用于检查目标元素的可见性。必须是目标元素的父级元素。如果未指定或者为null,则默认为浏览器视窗。
  • rootMargin: 配置 root 元素的 margin 值,写法同 css margin 写法。用来扩大检查相交的范围。默认值是 0。
  • threshold:相交门槛。可以是单一数值,也可以是数字数组。目标元素和 root 元素相交的范围达到该值的时候,触发回调,默认值是 0。
    • 0 表示只要有一个像素的相交就会触发回调函数
    • 1 表示完全相交才会触发回调函数
    • [0, 0.25, 0.5, 0.75, 1.0] 表示会触发 5 次回调函数,分别是,刚相交、相交范围达到 25%、相交范围达到 50%、相交范围达到 75%、完全相交的时候会触发回调函数。

callback

发生相交的回调

  • entries参数:返回当前已监听目标元素相交信息集合
  • observer参数:当前的观察者
js 复制代码
let callback = (entries, observer) => {
  entries.forEach((entry) => {
    console.log(entry);
  });
};

每一个 entry(相交信息) 有如下属性:

  • time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
  • target:被观察的目标元素
  • rootBoundsroot 元素的矩形区域的信息,和getBoundingClientRect()方法的返回值一致
  • boundingClientRect:目标元素的矩形区域的信息
  • intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
  • intersectionRatio:目标元素的可见比例 ,即intersectionRectboundingClientRect的比例,完全可见时为1,完全不可见时小于等于0
  • isIntersecting是否发生相交true表示相交,false表示没有相交

entries 是一个集合做一些补充:

上面案例中,监听的写法可以有下面两种:

  • 对每一个 img 创建一个 observer
js 复制代码
const imgs = document.querySelectorAll('img');
imgs.forEach((img) => {
  const observer = new IntersectionObserver((entry) => {});
  observer.observe(img);
});
  • 创建一个 observer,监听每一个 img
js 复制代码
const imgs = document.querySelectorAll('img');
const observer = new IntersectionObserver((entry) => {});
imgs.forEach((img) => {
  observer.observe(img);
});

当第二种写法的时候,元素是横向排列的,并且同时间相交的元素有多个,entries 就会返回每一个相交的元素,如下图

observer 实例

实例的observe方法可以指定观察哪个 DOM 节点。

javascript 复制代码
// 开始观察 el元素
observer.observe(el);

// 停止观察 el元素
observer.unobserve(el);

// 销毁观察器
observer.disconnect();

// 返回所有观察目标的 entry 对象数组
observer.takeRecords();

MutationObserver

变化观察者,可以很方便的 Dom 树的变动,如元素属性的变动,子节点的增删。只能监听到子元素的删除,监听节点的删除监听不到,所以需要监听当前节点删除的时候,需要监听父元素的子元素变化。

兼容性

提一嘴,MutationObserver 是这几个 Observer API中 兼容性最好的

polyfill

具体见链接readme说明,官方的polyfillgood

小案例

删不掉的水印,使用 MutationObserver 实现,对 body 进行监听,处理水印元素被删除的场景,监听水印元素的所有 dom 操作,用来防止被篡改。

代码

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>MutationObserver</title>
    <style>
      #watermark {
        width: 500px;
        height: 500px;
        border: 1px solid #000;
      }
    </style>
  </head>
  <body>
    <div id="watermark">假设我是一个水印节点</div>
    <script>
      const watermarkEl = document.getElementById('watermark');

      // 判断水印有没有被篡改
      const checkChange = (mutation) => {
        let flag = false;
        // 判断是不是删除了水印元素
        if (mutation.removedNodes.length) {
          flag = Array.from(mutation.removedNodes).some((node) => node === watermarkEl);
        }
        // 判断是不是修改了水印元素的属性
        if (mutation.type === 'attributes' && mutation.target === watermarkEl) {
          flag = true;
        }
        return flag;
      };

      const mo = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
          if (checkChange(mutation)) {
            alert('水印被篡改');
            // todo 重新生成水印
          }
        });
      });
      // 只监听 document 的子节点变化,用来处理水印节点被删除的场景
      mo.observe(document.body, {
        childList: true,
      });
      // 监听水印的属性、子节点、后代节点的变化
      mo.observe(watermarkEl, {
        childList: true, // 监听子节点 变化
        attributes: true, // 监听后代节点 变化
        subtree: true, // 监听属性 变化
        attributeFilter: ['style', 'class'], // 声明只监听这两属性
      });
    </script>
  </body>
</html>

预览

详细使用

js 复制代码
const observer = new MutationObserver(callback);

const targetNode = document.querySelector('#someElement');
const observerOptions = {
  childList: true, // 观察目标子节点的变化,是否有添加或者删除
  attributes: true, // 观察属性变动
  subtree: true, // 观察后代节点,默认为 false
};
observer.observe(targetNode, observerOptions);

构造函数

js 复制代码
const observer = new MutationObserver(callback);

observer 实例

js 复制代码
// 开始观察 el元素
observer.observe(el, observeOptions);

// 销毁观察器
observer.disconnect();

// 返回所有观察目标的 entry 对象数组
observer.takeRecords();

observeOptions:可选的配置对象,用来描述 DOM 的哪些变化应该触发回调。childListattributescharacterData 中,必须有一个参数为 true。否则会抛出 TypeError 异常。

  • subtree 可选,当为 true 时,将会监听以 target 为根节点的整个子树 。包括子树中所有节点的属性。默认值为 false

  • childList 可选,当为 true 时,监听 target 节点中发生的节点 的新增与删除。默认值为 false

  • attributes可选,当为 true 时观察所有监听的节点属性值的变化。默认值为 true,当声明了 attributeFilterattributeOldValue,默认值则为 false

  • attributeFilter可选,用于声明哪些属性名会被监听的数组。如果不声明该属性,所有属性的变化都将触发通知。

  • attributeOldValue可选,当为 true 时,记录上一次被监听的节点的属性变化;

  • characterData可选,当为 true 时,监听声明的 target 节点上所有字符的变化。默认值为 true,如果声明了 characterDataOldValue,默认值则为 false

  • characterOldValue可选,当为 true 时,记录前一个被监听的节点中发生的文本变化。,默认值则为 false

callback

js 复制代码
function callback(mutationList, observer) {
  // ...
}

回调函数有两个参数

  • mutationList: 描述所有被触发改动的MutationRecord对象数组
  • observer:当前的观察者

MutationRecord含有的属性介绍:

  • type: 变化的类型,attributes表示属性变化,characterData表示节点所有字符变化,childList表示子节点树变化。

  • target:发生变化的节点

  • addedNodes:如果是添加了节点,添加的节点会在这个属性表示

  • removedNodes:如果是移出了节点,移出的节点会在这个属性表示

  • previousSibling:返回被添加或移除的节点之前的兄弟节点,或者 null

  • nextSibling: 返回被添加或移除的节点之后的兄弟节点,或者 null

  • attributeName:返回被修改的属性的属性名,或者 null

  • attributeNamespace:返回被修改属性的命名空间,或者 null

  • oldValue:对于属性 attributes 变化,返回变化之前的属性值。对于 characterData 变化,返回变化之前的数据。对于子节点树 childList 变化,返回 null

ResizeObserver

尺寸变化观察者,可以很方便的监听元素大小的变化,元素display:none进行隐藏的时候,也是会触发监听的

兼容性

polyfill

具体见链接readme说明,社区实现的polyfill

小案例

代码

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ResizeObserver</title>
    <style>
      #roEl {
        width: 200px;
        height: 100px;
        border-image: linear-gradient(deepskyblue, deeppink) 1;
      }
    </style>
  </head>
  <body>
    <textarea id="roEl">酷炫边框</textarea>

    <script>
      const roEl = document.getElementById('roEl');
      const observe = new ResizeObserver(function (entries) {
        const entry = entries[0];
        const cr = entry.contentRect;
        const target = entry.target;

        const angle = cr.width - 200 + (cr.height - 100);
        target.style.borderImageSource = 'linear-gradient(' + angle + 'deg, deepskyblue, deeppink)';
      });
      observe.observe(roEl); // 观察文本域元素
    </script>
  </body>
</html>

预览

详细使用

js 复制代码
const observer = new ResizeObserver(callback);

const targetNode = document.querySelector('#someElement');
const observerOptions = {
  box: 'border-box', // 设置监听的盒模型
};
observer.observe(targetNode, observerOptions);

构造函数

js 复制代码
const observer = new ResizeObserver(callback);

observer 实例

js 复制代码
// 开始观察 el元素
observer.observe(el, observeOptions);

// 停止观察 el元素
observer.unobserve(el);

// 销毁观察器
observer.disconnect();

observeOptions:可选的配置对象,用来描述 DOM 的哪些变化应该触发回调。目前只支持box一个属性配置。

  • box:设置 observer 将监听的盒模型。可能的值是:
    • content-box(默认),CSS 中定义的内容区域的大小。
    • border-box,CSS 中定义的边框区域的大小。
    • device-pixel-content-box,在对元素或其祖先应用任何 CSS 转换之前,CSS 中定义的内容区域的大小,以设备像素为单位。

content box示意图:

如果指定的 boxcontent-box,那么我们修改padding或者 border-width,是不会触发回调函数的

callback

发生大小变化的回调

  • entrie参数:真正在观察的 Element 最新的大小。类型是 ResizeObserverEntry

ResizeObserverEntry类型介绍:

  • borderBoxSize:正在观察元素的新边框盒的大小。

  • contentBoxSize:正在观察元素的新内容盒的大小。

  • devicePixelContentBoxSize:正在观察元素的新内容盒的大小(以设备像素为单位)。

  • contentRect:正在观察元素新大小的 DOMRectReadOnly 对象。这是一个遗留属性,并且在未来的版本中可能被弃用。

  • target:对正在观察的 Element 。

PerformanceObserver

性能报告的观察者,用来采集页面的性能的,可以做性能上报,暂时没有涉及到相关代码,以后有机会在补充

ReportingObserver

浏览器使用过时的 api 或者浏览器对我们 api 执行有干预的时候会触发监听,暂时没有涉及到相关代码,以后有机会在补充

相关推荐
程序员大金13 分钟前
基于SSM+Vue+MySQL的酒店管理系统
前端·vue.js·后端·mysql·spring·tomcat·mybatis
清灵xmf16 分钟前
提前解锁 Vue 3.5 的新特性
前端·javascript·vue.js·vue3.5
Jiaberrr22 分钟前
教你如何在微信小程序中轻松实现人脸识别功能
javascript·微信小程序·小程序·人脸识别·百度ai
白云~️29 分钟前
监听html元素是否被删除,删除之后重新生成被删除的元素
前端·javascript·html
2401_8644769330 分钟前
无线领夹麦克风哪个降噪好?一文搞懂麦克风什么牌子的音质效果好
javascript·git·sql·github·mssql
金灰37 分钟前
有关JS下隐藏的敏感信息
前端·网络·安全
Yxmeimei38 分钟前
css实现居中的方法
前端·css·html
6230_40 分钟前
git使用“保姆级”教程2——初始化及工作机制解释
开发语言·前端·笔记·git·html·学习方法·改行学it
二川bro43 分钟前
Vue 修饰符 | 指令 区别
前端·vue.js
一只欢喜1 小时前
uniapp使用uview2上传图片功能
前端·uni-app