浏览器中好用的 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%、完全相交的时候会触发回调函数。

发生相交的回调

  • 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 执行有干预的时候会触发监听,暂时没有涉及到相关代码,以后有机会在补充

相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅10 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端
爱敲代码的小鱼10 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax