使用IntersectionObserver实现元素观察

8,568 阅读5分钟

背景

写这一篇文章的目的主要也是自己学习记录,以前对这个api还是比较陌生的,最近项目上做性能优化的时候,考虑到了这个监听api,参考各种资料文章,写了这一篇文章,希望对自己或者对大家参考有所帮助。

本文章所有用例使用的都是react

1、IntersectionObserver对象

IntersectionObserver接口提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。

当一个IntersectionObserver对象被创建时,其被配置为监听根中一段给定比例的可见区域。一旦IntersectionObserver被创建,则无法更改其配置,所以一个给定的观察者对象只能用来监听可见区域的特定变化值;然而,你可以在同一个观察者对象中配置监听多个目标元素。

2、使用

2.1 、接口定义


declare var IntersectionObserver: {
    prototype: IntersectionObserver;
    new(callback: IntersectionObserverCallback, options?: IntersectionObserverInit): IntersectionObserver;
};

2.2、创建一个监听对象,并监听id为id的标签


const observer = new IntersectionObserver(callback, options)

observer.observe(document.getElementById('id'))

2.3、IntersectionObserver对象参数callback和options

2.3.1 callback

接口定义

当元素可见比例超过指定阈值后,会调用一此回调函数,这个回调函数有两个参数enters和observer


interface IntersectionObserverCallback {
    (entries: IntersectionObserverEntry[], observer: IntersectionObserver): void;
}

enters: 一个IntersectionObserverEntry对象的数组,每个被触发的阈值,都或多或少与指定阈值有偏差。

observer: 被调用的IntersectionObserver实例

IntersectionObserverEntry对象

描述了目标元素与其根元素容器在某一特定过渡时刻的交叉状态. IntersectionObserverEntry 的实例作为 entries 参数被传递到一个 IntersectionObserver 的回调函数中; 此外, 这些对象只能通过调用IntersectionObserver.takeRecords() 来获取.

/** This Intersection Observer API interface describes the intersection between the target element and its root container at a specific moment of transition. */
interface IntersectionObserverEntry {
    readonly boundingClientRect: DOMRectReadOnly;
    readonly intersectionRatio: number;
    readonly intersectionRect: DOMRectReadOnly;
    readonly isIntersecting: boolean;
    readonly rootBounds: DOMRectReadOnly | null;
    readonly target: Element;
    readonly time: DOMHighResTimeStamp;
}

属性

  • boundingClientRect 返回包含目标元素(被检测的元素)的边界信息的DOMRectReadOnly. 边界的计算方式与 Element.getBoundingClientRect() 相同.
  • intersectionRatio 返回intersectionRect 与 boundingClientRect 的比例值.
  • intersectionRect 返回一个 DOMRectReadOnly 用来描述根和目标元素的相交区域.
  • isIntersecting 返回一个布尔值, 如果目标元素与交叉区域观察者对象的根相交,则返回 true .如果返回 true, 相交返回true,不相交返回false
  • rootBounds 返回一个 DOMRectReadOnly 用来描述交叉区域观察者中的根.
  • target 被检测的元素
  • time 返回一个记录从 IntersectionObserver 的时间原点到交叉被触发的时间的时间戳

image.png

2.3.2 options

一个可以用来配置observer实例的对象。如果options未指定,observer实例默认使用文档视口作为root,并且没有margin,阈值为0%(意味着即使一像素的改变都会触发回调函数)。

接口定义


interface IntersectionObserverInit {
    root?: Element | Document | null;
    rootMargin?: string;
    threshold?: number | number[];
}

root: 监听元素的祖先元素Element对象,如果未传入值或值为null,则默认使用顶级文档的视窗。

rootMargin: 计算交叉时添加到根(root)边界盒bounding box的矩形偏移量, 可以有效的缩小或扩大根的判定范围从而满足计算需要。此属性返回的值可能与调用构造函数时指定的值不同,因此可能需要更改该值,以匹配内部要求。所有的偏移量均可用像素(pixel)(px)或百分比(percentage)(%)来表达, 默认值为"0px 0px 0px 0px"。

未命名文件.png

threshold: 一个包含阈值的列表, 按升序排列, 列表中的每个阈值都是监听对象的交叉区域与边界区域的比率。当监听对象的任何阈值被越过时,都会生成一个通知(可以理解为触发一次callback)。如果构造器未传入值, 则默认值为0。

2.4 实例方法

IntersectionObserver.disconnect(): 使IntersectionObserver对象停止监听工作

IntersectionObserver.observe(): 使IntersectionObserver开始监听一个目标元素。

IntersectionObserver.takeRecords(): 返回所有观察目标的IntersectionObserverEntry对象数组。当观察到交互动作发生时,回调函数并不会立即执行,而是在空闲时期使用 requestIdleCallback 来异步执行回调函数,但是也提供了同步调用的 takeRecords 方法。

IntersectionObserver.unobserve(): 使IntersectionObserver停止监听特定目标元素。

使用举例

此案例监听最下面div标签,当监听到在视窗内,打印提示。并且停止对此div的监听

const listData = Array.from({ length: 50 }, (_, i) => i + 1 )

function ObserverTestPage() {

  const observeRef = useRef<HTMLDivElement | null>(null)

  useEffect(() => {
    const callback: IntersectionObserverCallback = (enters) => {
      for (const item of enters) {
        if (item.isIntersecting) {
          console.log('监听的dom出现了====')
          // 第一次出现之后,后续停止对此dom的监听
          observe.unobserve(item.target)
        }
      }
    }
    const observe = new IntersectionObserver(callback)

    observe.observe(observeRef)

    // 组件销毁的时候,停止所有监听
    return () => observe.disconnect()
  }, [])

  return (
    <div className="ImageLazy">
      <div className="plac">
        {listData.map(v => <div key={v}>{v}</div>)}
      </div>
      <div ref={observeRef} className="observe">
        this is dom
      </div>
    </div>
  )
}

监听1.gif

3、使用场景

3.1 实现图片懒加载


import React, { useEffect, useRef } from 'react'
import { imgList } from './constant'
import './ImageLazy.less'

const listData = Array.from({ length: 50 }, (_, i) => i + 1 )

function ObserverTestPage() {

  const observeRef = useRef<HTMLDivElement | null>(null)

  useEffect(() => {
    const callback: IntersectionObserverCallback = (enters) => {
      for (const item of enters) {
        const { isIntersecting, target }: IntersectionObserverEntry & { target: any } = item
        if (isIntersecting) {
          if (target.nodeName === 'IMG' && !target.src) {
            target.src = target.dataset.src
            observe.unobserve(target)
          }
        }
      }
    }
    const observe = new IntersectionObserver(callback)
    const imgTag = document.querySelectorAll('img[data-src]')
    imgTag.forEach(v => observe.observe(v))

    // 组件销毁的时候,停止所有监听
    return () => observe.disconnect()
  }, [])

  return (
    <div className="ImageLazy">
      <div className="plac">
        {listData.map(v => <div key={v}>{v}</div>)}
      </div>
      <div ref={observeRef} className="observe">
        this is dom
      </div>
      <div className="image-content">
        {imgList.map((v, i) => (
          <div key={i} className="image-wrap">
            <img data-src={v} alt="" />
          </div>
        ))}
      </div>
    </div>
  )
}

export default ObserverTestPage

监听2.gif

3.2 监听元素是否出现在视口中(react实现案例)

这里是ahooks写法useInViewport写法

type InViewport = boolean | undefined;

function isInViewPort(el: HTMLElement): InViewport  {
  if (!el) {
    return undefined;
  }

  const viewPortWidth =
    window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
  const viewPortHeight =
    window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
  const rect = el.getBoundingClientRect();

  if (rect) {
    const { top, bottom, left, right } = rect;
    return bottom > 0 && top <= viewPortHeight && left <= viewPortWidth && right > 0;
  }

  return false;
}

function useInViewport(target: BasicTarget): InViewport {
  const [inViewPort, setInViewport] = useState<InViewport>(() => {
    const el = getTargetElement(target);

    return isInViewPort(el as HTMLElement);
  });

  useEffect(() => {
    const el = getTargetElement(target);
    if (!el) {
      return () => {};
    }

    const observer = new IntersectionObserver((entries) => {
      for (const entry of entries) {
        if (entry.isIntersecting) {
          setInViewport(true);
        } else {
          setInViewport(false);
        }
      }
    });

    observer.observe(el as HTMLElement);

    return () => {
      observer.disconnect();
    };
  }, [target]);

  return inViewPort;
}

4、兼容性

image.png

数据来源can i use

5、总结

总体来说IntersectionObserver对象的可操作性还是挺不错的,在检测元素进入视口区域来说提供了极大的便利性,可以用来处理图片懒加载,组件懒加载等。但是在使用的时候需要考虑兼容性(可以使用intersection-observer-polyfill解决)

参考

1.MDN-Intersection Observer

2.一个神奇的交叉观察 API Intersection Observer

3. ahhoks-useInViewport

4. can i use