利用Intersection实现下拉加载数据

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

在我们进行业务开发的过程中,常常会碰到下拉加载列表数据的需求。本文将介绍如何利用Intersection API实现一个简单的下拉加载数据的demo。

传统的下拉加载方案

传统的下拉加载方案大多数都是通过监听scroll事件,然后获取目标元素坐标以及相关数据,再进行对应的实现。例如下面就是一个依赖数据列表容器的scrollHeightscrollTopheight实现的下拉加载的demo。

代码实现

function App() {
  // 用于记录当前是否正在请求中
  const loadingRef = useRef<boolean>(false);
  // 列表容器
  const containerRef = useRef<HTMLDivElement>(null);
  const [dataList, setDataList] = useState([]);

  useEffect(() => {
    fetchData();
  }, []);

  useEffect(() => {
    const { height } = containerRef.current.getBoundingClientRect();
    const scrollHeight = containerRef.current.scrollHeight;

    const onScroll = () => {
      console.log('scrollHeight:', scrollHeight, 'scrollTop:', containerRef.current.scrollTop, 'height:', height);
      if (scrollHeight - containerRef.current.scrollTop - 1 <= height) {
        // 当容器已经拉到最底部时,发起请求
        fetchData();
      }
    };

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

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

  const fetchData = () => {
    // 模拟数据请求
    // 如果当前正在请求中,直接返回
    if (loadingRef.current) return;
    // 标记当前正在请求中
    loadingRef.current = true;
    setTimeout(() => {
      setDataList(_dataList => {
        const dataList = [..._dataList];
        for (let i = 0; i < 20; i++) {
          dataList.push(Math.random());
        }

        return dataList;
      });
      loadingRef.current = false;
    }, 500);
  };

  return (
    <div ref={containerRef} className="list-container">
      {dataList.map(item => (
        <p className="list-item" key={item}>
          {item}
        </p>
      ))}
      <div className="loading">loading...</div>
    </div>
  );
}

实现效果

2021-12-26 17-06-14 00_00_02-00_00_07.gif

存在问题

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

// 节流
throttle(onScroll, 500);

2.scrollTop的小数问题
眼尖的同学可能已经看到的,我们在判断容器是否已经滚动到底部是,还做了一个-1的操作。

if (scrollHeight - containerRef.current.scrollTop - 1 <= height) {
  // 当容器已经拉到最底部时,发起请求
  fetchData();
}

这是因为在使用显示比例缩放的系统上,scrollTop可能会提供一个小数。如下图所示,在容器滚动到底部时,scrollHeight(1542) - scrollTop(1141.5999755859375) 与容器的高度height(400)并不相等。

image.png

所以我们需要做出相应的兼容处理。

Intersection版本下拉加载

Intersection

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

IntersectionObserver的用法十分简单,我们只需要定义好DOM元素的可视状态发生变化后需要做些什么,以及需要观察哪些元素的可视状态就好了。

接下来我们详细的看看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实例四个方法:

Intersection的优势

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

实现思路

我们在实现下拉加载功能时,当数据列表还没有加载完时,我们往往会在数据列表的最后放置一个loading组件,表示当数据列表还有更加数据,并且正在加载中。我们可以利用这个loading组件的可视状态以及Intersection API实现Intersection版本的下拉加载。

代码实现

function App() {
  // 用于记录当前是否正在请求中
  const loadingRef = useRef<boolean>(false);
  // loading div
  const loadingDivRef = useRef<HTMLDivElement>(null);
  const [dataList, setDataList] = useState([]);

  useEffect(() => {
    fetchData();
  }, []);

  useEffect(() => {
    let intersectionObserver = new IntersectionObserver(function (entries) {
      if (entries[0].intersectionRatio > 0) {
        // intersectionRatio大于0,代表监听的元素由不可见变成可见,进行数据请求
        fetchData();
      }
    });

    // 监听Loading div的可见性
    intersectionObserver.observe(loadingDivRef.current);

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

  const fetchData = () => {
    // 模拟数据请求
    // 如果当前正在请求中,直接返回
    if (loadingRef.current) return;
    // 标记当前正在请求中
    loadingRef.current = true;
    setTimeout(() => {
      setDataList(_dataList => {
        const dataList = [..._dataList];
        for (let i = 0; i < 20; i++) {
          dataList.push(Math.random());
        }

        return dataList;
      });
      loadingRef.current = false;
    }, 500);
  };

  return (
    <div className="list-container">
      {dataList.map(item => (
        <p className="list-item" key={item}>
          {item}
        </p>
      ))}
      <div ref={loadingDivRef} className="loading">
        loading...
      </div>
    </div>
  );
}

实现效果

2021-12-26 15-13-22 00_00_03-00_00_06.gif
上一篇下一篇

猜你喜欢

热点阅读