浅入浅出IntersectionObserver并实现一个简单的

2021-12-27  本文已影响0人  CBDxin

在日常的业务开发当中,我们常常需要依赖DOM元素的可见性来完成某些需求,如图片的懒加载、数据列表的下拉加载等场景。

传统的实现方式

传统的/DOM元素的可见性检测方案大多数都是通过监听容器元素的scroll事件,然后获取目标元素坐标以及相关数据,最后才能判断当前元素是否可视。例如下面是一个判断div.target元素是否在他的容器div.container中可视的例子。

function Visible() {
  const [visible, setVisible] = useState(false);
  const targetRef = useRef<HTMLDivElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const targetHeight = targetRef.current.getBoundingClientRect().height;
    const containerHeight = containerRef.current.getBoundingClientRect().height;
    const scrollHeight = containerRef.current.scrollHeight;

    const onScroll = () => {
      if (scrollHeight - containerRef.current.scrollTop - containerHeight > targetHeight) {
        setVisible(false);
      } else {
        setVisible(true);
      }
    };

    containerRef.current.addEventListener('scroll', onScroll);

    return () => {
      containerRef.current.removeEventListener('scroll', onScroll);
    };
  }, []);

  return (
    <div>
      <div>target是否可见:{visible ? '是' : '否'}</div>
      <div
        ref={containerRef}
        style={{
          height: 300,
          overflow: 'auto',
        }}
        className="container">
        <div style={{ height: 500 }}></div>
        <div style={{ backgroundColor: 'yellow', height: 100 }} className="target" ref={targetRef}>
          target
        </div>
      </div>
    </div>
  );
}
2021-12-26 21-50-09 00_00_03-00_00_09.gif

我们知道,scroll事件的发生是十分密集的,而我们在监听scroll事件的回调函数中,还需要去获取容器的scrollTop这会导致“重排”的发生。此时需要我们额外去做一些防抖或是节流的工具,防止造成性能问题。

IntersectionObserver

IntersectionObserver 提供了一种异步观察目标元素在其祖先元素或顶级文档视窗(viewport)中是否可视的方法。

IntersectionObserver的用法十分简单,我们只需要定义好DOM元素的可视状态发生变化后需要做些什么,以及需要观察哪些元素的可视状态就好了。下面的代码同样完成可视检测的功能。

function Visible() {
  const [visible, setVisible] = useState(false);
  const targetRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    let intersectionObserver = new IntersectionObserver(function (entries) {
      // 定义DOM元素的可视状态发生变化后需要做些什么
      if (entries[0].intersectionRatio > 0) {
        // intersectionRatio大于0,代表监听的元素由不可见变成可见
        setVisible(true);
      } else {
        // 反之则代表监听的元素由可见变成不可见
        setVisible(false);
      }
    });

    // 监听target元素的可见性
    intersectionObserver.observe(targetRef.current);

    return () => {
      intersectionObserver.unobserve(targetRef.current);
      intersectionObserver.disconnect();
      intersectionObserver = null;
    };
  }, []);

  return (
    <div>
      <div>target是否可见:{visible ? '是' : '否'}</div>
      <div
        style={{
          height: 300,
          overflow: 'auto',
        }}
        className="container">
        <div style={{ height: 500 }}></div>
        <div style={{ backgroundColor: 'yellow', height: 100 }} className="target" ref={targetRef}>
          target
        </div>
      </div>
    </div>
  );
}

使用intersectionObserver后,除了代码变得简短了之外,intersectionObserver构造函数中传入的回调函数只会在观察的元素的可视状态发生变化后才会执行,很好的解决传统判断可视的方案的性能瓶颈。

接下来我们详细的看看intersectionObserver这个API。

const intersectionObserver = new IntersectionObserver(callback, options?) ;

IntersectionObserver构造函数会接收两个参数。

callback

callback为被观察元素的可视状态发生变更后的回调函数,此回调函数接受两个参数:

function callback(entries, observer?) => {
  //...
}

entries:一个IntersectionObserverEntry对象的数组。IntersectionObserverEntry对象用于描述被观察对象的可视状态的变化,拥有以下的属性:

observer:当前IntersectionObserver实例的引用。

options

options为一个可选参数,可传入以下属性:

IntersectionObserver实例

IntersectionObserver构造函数会把options中的属性挂载到IntersectionObserver实例上,并赋予IntersectionObserver实例四个方法:

使用IntersectionObserver实现一个简单的图片懒加载组件

实现思路

在页面加载时我们不会去加载img标签的图片资源,只有在img标签已经可视了,我们才会去加载图片资源。

实现代码

import { useEffect, useRef, useState } from 'react';

function LazyImg(props) {
  // 单独将src从props中提取出来
  const { src, ...restProps } = props;
  // 标记是否已经进入过可视范围
  const [loaded, setLoaded] = useState(false);
  const imgRef = useRef<HTMLImageElement>(null);

  useEffect(() => {
    let intersectionObserver = new IntersectionObserver(function (entries) {
      // 定义DOM元素的可视状态发生变化后需要做些什么
      if (entries[0].intersectionRatio > 0) {
        // intersectionRatio大于0,代表监听的元素由不可见变成可见
        setLoaded(true);

        // 加载过后,后续无需继续观察img的可视状态,进行解绑操作
        intersectionObserver.unobserve(imgRef.current);
        intersectionObserver.disconnect();
        intersectionObserver = null;
      }
    });

    // 监听target元素的可见性
    intersectionObserver.observe(imgRef.current);

    return () => {
      if (intersectionObserver) {
        intersectionObserver.unobserve(imgRef.current);
        intersectionObserver.disconnect();
        intersectionObserver = null;
      }
    };

    // src出现变化时需要重新进行绑定
  }, [src]);

  // 只有当loaded为true时才去加载src
  return <img {...restProps} ref={imgRef} src={loaded ? src : ''} />;
}

export default LazyImg;

实现效果

2021-12-27 08-52-03 00_00_02-00_00_09.gif
上一篇 下一篇

猜你喜欢

热点阅读