乐趣区

观察者们

Intersection Observer

Intersection Observer API提供了一种异步观察目标元素与祖先元素或顶级文档 viewport 的“交集 ” 中的变化的方法。

兼容性

介绍

一直以来,检测元素的可视状态或者两个元素的相对可视状态都不是件容易事,毕竟大部分解决办法并非完全可靠,也极易拖慢整个网站的性能。然而,随着网页发展,对上述检测的需求也随之增加了。多种情况下都需要用到元素交集变化的信息,比如:

  • 当页面滚动时,懒加载图片或其他内容。
  • 实现“可无限滚动”网站,也就是当用户滚动网页时直接加载更多内容,无需翻页。
  • 为计算广告收益,检测其广告元素的曝光情况。
  • 根据用户是否已滚动到相应区域来灵活开始执行任务或动画。

以往我们的做法是绑定容器的 scroll 事件,或者设定时器不停地调用getBoundingClientRect() 获取元素位置,但是这些代码都是在主线程上运行。所以这样做的性能会有一定的影响。

我们现在的 mall-core 里的 imglazyload 就是用的传统的方式

API

var observer = new IntersectionObserver(callback, options)

以上代码会返回一个 IntersectionObserver 实例,callback 是当元素的可见性变化时候的回调函数,options 是一些配置项(可选)

返回的这个实例呢,也比较简单,有三个方法。

  • observe 观察某个元素 observer.observe(ele)
  • unobserve 停止观察某个元素 observer.unobserve(ele)
  • disconnect 关闭观察器 observer.disconnect()

Intersection observer options
传递到 IntersectionObserver()构造函数的 options 对象,允许控制调用观察者的回调的环境。它也是有 3 个字段

  • root
    指定根 (root) 元素,用于检查目标的可见性。必须是目标元素的父级元素。如果未指定或者为 null,则默认为浏览器视窗。用作父级元素的时候,取值为父级元素的getboundingClientRect()
  • rootMargin
    root 元素的外边距。类似于 css 中的 margin 属性,比如 “10px 20px 30px 40px” (top, right, bottom, left)。如果有指定 root 参数,则 rootMargin 也可以使用百分比来取值。该属性值是用作 root 元素和 target 发生交集时候的计算交集的区域范围,使用该属性可以控制 root 元素每一边的收缩或者扩张。默认值为 0。
    用图来解释

通过该图就可以知道,原本被观察的元素在通过 rootMargin 扩大后,会提前触发 callback
  • threshold
    用来指定交叉比例,决定什么时候触发回调函数,是一个数组,默认是 0,

设置为 [0, 0.5, 1] 就是指当元素出现 0%、50%、100% 时都会触发 callback

callback
当元素的可见性发生变化时,就会触发 callback 函数。

function callback(entries, observer) {
    // 回调接受两个参数,一个是 IntersectionObserverEntry 数组,一个是 obsever 自己
    for (var i = 0; i < entries.length; i++) {console.log(entries[i]);
    }
}
  • boundingClientRect 目标元素的矩形信息
  • intersectionRatio 相交区域和目标元素的比例值
  • intersectionRect/boundingClientRect 不可见时小于等于 0
  • intersectionRect 目标元素和视窗(根)相交的矩形信息 可以称为相交区域
  • isIntersecting 目标元素当前是否可见 Boolean 值 可见为 true
  • isvisible 感觉一直都是 false,官方也没有介绍
  • rootBounds 根元素的矩形信息,没有指定根元素就是当前视窗的矩形信息
  • target 观察的目标元素
  • time 返回一个记录从 IntersectionObserver 的时间到交叉被触发的时间的时间戳

请留意,你注册的回调函数将会在主线程中被执行。所以该函数执行速度要尽可能的快。如果有一些耗时的操作需要执行,建议使用 Window.requestIdleCallback() 方法。

需要注意的是
由于观察名叫交叉,所以第一思维会是和边相交,很容易理解为元素和根元素的边相交,实际上这里的触发方式并不是说一定就是和根元素的边相交,而是元素的可见性出现在根元素整体视窗内就算相交。

接下来,就用这个来做一个简易的懒加载模块

核心代码

import {useEffect} from 'react';
function loadImg(entries, observer) {for (var i = 0; i < entries.length; i++) {const img = entries[i].target;
        var src = img.getAttribute("data-src");
        console.log(entries[i]);
        if (entries[i].isIntersecting) {
            img.src = src;
            
            img.removeAttribute("data-src");
            img.classList.remove('imglazy');
            observer.unobserve(img);
            // 实验 0.5,1 解开
        }
    }
}
function observerImgs(className, observer) {const imgs = document.querySelectorAll(`img.${className}`);
    if(!imgs.length) {return;}
    imgs.forEach((img) => {observer.unobserve(img);
        observer.observe(img);
    });
}
function useImgLazy(className, list) {useEffect(() => {
        const observer = new IntersectionObserver(loadImg,{// root: document.querySelector('.product_list'),
            // rootMargin: '0px',
            threshold: [0.5, 1]
        });
        observerImgs(className, observer);
        return () => {observer.disconnect();
        };
    }, [className, list]);
}
export default useImgLazy;

为了观察更直观,所以 demo 是用的 0.5 露出才变更 src

退出移动版