IntersectionObserver

1,774 阅读4分钟

我们在网页开发中,经常需要监听某个元素是否进入了可视区域内,从而进行相关操作,例如懒加载等;之前的做法大多都是通过监听 scroll 事件,通过获取目标元素的当前位置与视窗位置进行判断,通过这种方法需要监听 scroll 事件并且同时需要获取元素当前位置,会进行大量计算重绘等操作,可能会使页面卡顿,降低用户体验。

IntersectionObserver 接口,可以代替我们手动监听元素,可以自动“观察”元素是否可见。

概念(引自 MDN

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

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

const observer = new IntersectionObserver(callback: IntersectionObserverCallback, options?: IntersectionObserverInit): IntersectionObserver;

observer.observe(target);

// 最简单的一个使用示例,获取要监听的元素,新建 IntersectionObserver 实例,观察要监听元素,一个根元素可以同时监听多个元素
const divDom = document.getElementsByClassName('hidden');
const observer = new IntersectionObserver(
    (entries) => {
        entries.forEach((entry) => {
            // 过滤掉未出现在视窗中的元素
            if (entry.intersectionRatio <= 0) {
                return;
            }
            console.log(entry);
        },
        { threshold: [0.2, 0.25, 0.5, 1] },);
    }
);

observer.observe(divDom[0]);
observer.observe(divDom[1]);

参数

new IntersectionObserver(callback: IntersectionObserverCallback, options?: IntersectionObserverInit): IntersectionObserver)
  • callback

    当元素可见比例超过指定阈值后,会调用传入的回调函数,回调函数有两个参数

    此回调函数会在主线程中运行,所以此函数的执行应该尽量快,不然会导致卡顿等性能问题,可以使用异步方法或者 requestIdleCallbackrequestAnimationFrame 等方法进行优化

    • entries

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

      IntersectionObserverEntry

      • time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
      • target:被观察的目标元素,是一个 DOM 节点对象
      • rootBounds:容器元素的矩形区域的信息,getBoundingClientRect()方法的返回值,如果没有容器元素(即直接相对于视口滚动),则返回null
      • boundingClientRect:目标元素的矩形区域的信息
      • intersectionRect:目标元素与视口(或容器元素)的交叉区域的信息
      • intersectionRatio:目标元素的可见比例,即intersectionRectboundingClientRect的比例,完全可见时为1,完全不可见时小于等于0
    • observer

      被调用的IntersectionObserver实例。

  • options 可选

    • root必须是监听目标对象祖先元素,若不指定则默认根元素。
    • rootMargin一个在计算交叉值时添加至根的边界盒 (bounding_box (en-US)) 中的一组偏移量,类型为字符串 (string) ,可以有效的缩小或扩大根的判定范围从而满足计算需要。语法大致和 CSS 中的margin 属性等同; 可以参考 The root element and root margin in Intersection Observer API来深入了解 margin 的工作原理及其语法。默认值是"0px 0px 0px 0px"。
    • threshold规定了一个监听目标与边界盒交叉区域的比例值,可以是一个具体的数值或是一组 0.0 到 1.0 之间的数组。若指定值为 0.0,则意味着监听元素即使与根有 1 像素交叉,此元素也会被视为可见。若指定值为 1.0,则意味着整个元素都在可见范围内时才算可见。可以参考Thresholds in Intersection Observer API 来深入了解阈值是如何使用的。阈值的默认值为 0.0。

返回值

一个可以使用规定阈值监听目标元素可见部分与root交叉状况的新的IntersectionObserver 实例。调用自身的observe() 方法开始使用规定的阈值监听指定目标,例如observer.observe(divDom[0]);

使用

IntersectionObserver还是有很大的用途,可以取代通过onscroll事件来监听滚动高度与目标元素之间的交互,不在完全依赖onscroll,可以提高我们页面的性能。

  • 无限滚动 通过监听列表最下方的元素,当元素出现之后,获取下一页数据,监听数据的变化,重新监听列表页最下方的元素。

    const observerRef = useRef<IntersectionObserver | null>();
    
    useEffect(() => {
        // 监听滚动事件,当页面滚动到底部的时候要获取下一页数据
        if (canUseIntersectionObserver()) {
            observerRef.current = new IntersectionObserver((entries) => {
                if (entries[0].intersectionRatio <= 0) {
                    return;
                }
    
                // 获取下一页数据,当前的元素已经触发过获取新数据事件,需要当前监听的元素停止监听
                fetchNextFn();
                observerRef.current!.unobserve(entries[0].target);
            });
        }
    
        return () => {
            // 离开时清除状态
            observerRef.current = null;
        };
    }, []);
    
    // 监听滚动最下方的时候的事件
    // 上面 fetchNextFn 触发之后,获取到的数据发生变化,要重新监听新列表最下方的元素
    useEffect(() => {
        if (observerRef.current) {
            observerRef.current!.observe(observerElement as Element);
        }
        // 监听变化的列表数据
    }, [observerValue]);
    
  • 懒加载 可以代替之前使用scroll事件来实现懒加载,通过IntersectionObserver来实现有更高的性能,当元素出现在视窗中即可触发图像加载

    <img src="placeholder.png" data-src="img-1.jpg">
    <img src="placeholder.png" data-src="img-2.jpg">
    <img src="placeholder.png" data-src="img-3.jpg">
    
    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if(entry.intersectionRatio <= 0) {
                return;
            }
    
            // 懒加载图片,完成后从监听列表中删除
            entry.target.src = entry.target.dataset.src;
            observer.unobserve(entry.target);
        });
    });
    
    // 监听所需要懒加载的img 标签
    Array.prototype.slice.call(document.getElementsByTagName('img')).forEach(img => {
        observer.observe(img);
    });
    
  • 依据滚动高度修改样式

    function getThresholdList(steps?: number) {
        const thresholds = [0];
        const numSteps = steps || 20;
    
        for (let i = 1.0; i <= numSteps; i++) {
            let ratio = i / numSteps;
            thresholds.push(ratio);
        }
    
        return thresholds;
    }
    
    const headerDom = document.getElementsByClassName('header');
    const observer = new IntersectionObserver(
        (entries) => {
            if (entries[0].intersectionRatio <= 0) {
                return;
            }
            // 在此处修改样式
            modifyStyle(entries[0])
        },
        { threshold: buildThresholdList() },
    );
    
    observer.observe(headerDom[0]);
    
  • 不止于此,自动播放暂停等等,都可以尝试使用。

实际使用

此 api 较新,但是在 2022.6.15 IE 正式不维护之后,其他浏览器可以尝试使用,微信小程序中也有支持此 api,当浏览器不支持此 api 时,我们也可以降级使用 onscroll 事件,可以使用下面的函数来判断浏览器是否支持,支持的话,我们就可以愉快滴使用了。

const canUseIntersectionObserver = () =>
    'IntersectionObserver' in window &&
    'IntersectionObserverEntry' in window &&
    'intersectionRatio' in window.IntersectionObserverEntry.prototype;

参考