浏览器中好用的 Observer Api
简介
网页开发中经常会处理用户交互相关的事件。随着我们的需求越来越复杂,有很多的场景浏览的事件机制不能很好,或者说不能很快速的实现我们的需求。比如:
- 
页面水印需要防止水印元素被篡改。
 - 
在实现图片懒加载的时候,我们需要判断元素是否出现在了可视区域内。
 - 
在一些复杂的交互中,我们需要判断元素的大小是否发生了变化。
 
目前浏览器提供了 5 种很好用的 Observer Api,可以很方便的实现上面的需求。
- 
MutationObserver:用来监听
DOM树的变动 - 
IntersectionObserver:用来监听两个元素是否相交
 - 
ResizeObserver:用来监听元素的大小更改
 - 
PerformanceObserver:浏览器渲染页面过程中有一些关键的渲染时间点,可以用这个
api监听到 - 
ReportingObserver:浏览器使用过时的
api或者浏览器对我们api执行有干预的时候会触发监听 
Observer Api 属于微任务 ,优先级小于 Promise,每一个Observer 在创建的时候会调用一次,然后每次监听的相关事件触发的时候会执行回调。
接下来我会对这几个 API 的使用、兼容性、polyfill 做一些介绍。
IntersectionObserver
相交观察者,可以很方便的检测一个元素是否可见或者两个元素是否相交
兼容性

polyfill
具体见链接readme说明,官方的polyfill,good
小案例
图片懒加载案例,核心逻辑是将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:被观察的目标元素rootBounds:root元素的矩形区域的信息,和getBoundingClientRect()方法的返回值一致boundingClientRect:目标元素的矩形区域的信息intersectionRect:目标元素与视口(或根元素)的交叉区域的信息intersectionRatio:目标元素的可见比例 ,即intersectionRect占boundingClientRect的比例,完全可见时为1,完全不可见时小于等于0isIntersecting:是否发生相交 ,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说明,官方的polyfill,good
小案例
删不掉的水印,使用 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 的哪些变化应该触发回调。childList、attributes 和 characterData 中,必须有一个参数为 true。否则会抛出 TypeError 异常。
- 
subtree可选,当为true时,将会监听以target为根节点的整个子树 。包括子树中所有节点的属性。默认值为false。 - 
childList可选,当为true时,监听target节点中发生的节点 的新增与删除。默认值为false。 - 
attributes可选,当为true时观察所有监听的节点属性值的变化。默认值为true,当声明了attributeFilter或attributeOldValue,默认值则为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示意图:

如果指定的 box 为content-box,那么我们修改padding或者 border-width,是不会触发回调函数的
callback
发生大小变化的回调
entrie参数:真正在观察的Element最新的大小。类型是ResizeObserverEntry。
ResizeObserverEntry类型介绍:
- 
borderBoxSize:正在观察元素的新边框盒的大小。 - 
contentBoxSize:正在观察元素的新内容盒的大小。 - 
devicePixelContentBoxSize:正在观察元素的新内容盒的大小(以设备像素为单位)。 - 
contentRect:正在观察元素新大小的DOMRectReadOnly对象。这是一个遗留属性,并且在未来的版本中可能被弃用。 - 
target:对正在观察的 Element 。 
PerformanceObserver
性能报告的观察者,用来采集页面的性能的,可以做性能上报,暂时没有涉及到相关代码,以后有机会在补充
ReportingObserver
浏览器使用过时的 api 或者浏览器对我们 api 执行有干预的时候会触发监听,暂时没有涉及到相关代码,以后有机会在补充