ahooks源码解析之useInfiniteScroll

882 阅读2分钟

一、useInfiniteScroll的介绍和用法

useInfiniteScroll 封装了常见的无限滚动逻辑。

const { data, loading, loadingMore, loadMore } = useInfiniteScroll(service);

useInfiniteScroll 的第一个参数 service 是一个异步函数,对这个函数的入参和出参有如下约定:

  1. service 返回的数据必须包含 list 数组,类型为 { list: any[], ...rest }
  2. service 的入参为整合后的最新 data

假如第一次请求返回数据为 { list: [1, 2, 3], nextId: 4 }, 第二次返回的数据为 { list: [4, 5, 6], nextId: 7 }, 则我们会自动合并 list,整合后的的 data{ list: [1, 2, 3, 4, 5, 6], nextId: 7 }

二、源码解析

1、参数和配置项

useInfiniteScroll的第一个参数是请求服务,第二个参数是配置项。

const useInfiniteScroll = <TData extends Data>(
  service: Service<TData>,
  options: InfiniteScrollOptions<TData> = {},
) { ... }

我们来看看配置项:

const {
    target, // 滚动加载时的容器
    isNoMore, // 是否还有更多数据
    threshold = 100, // 滚动的边界
    reloadDeps = [], // 重置请求时的依赖项
    manual,
    onBefore,
    onSuccess,
    onError,
    onFinally,
 } = options;

2、请求的逻辑

判断是否没有更多数据的方法是外面传进来的:

// 判断是否没有更多了
  const noMore = useMemo(() => {
    if (!isNoMore) return false;
    return isNoMore(finalData);
  }, [finalData]);

接下来我们看看执行请求的逻辑:

const { loading, run, runAsync, cancel } = useRequest(
    // 请求数据,并且和旧的数据合并
    async (lastData?: TData) => {
      const currentData = await service(lastData);
      if (!lastData) {
        setFinalData(currentData);
      } else {
        setFinalData({
          ...currentData,
          // @ts-ignore
          list: [...lastData.list, ...currentData.list],
        });
      }
      return currentData;
    },
    // 执行`useInfiniteScroll`的各个钩子,设置LoadingMore状态
    {
      manual,
      onFinally: (_, d, e) => {
        setLoadingMore(false);
        onFinally?.(d, e);
      },
      onBefore: () => onBefore?.(),
      // 请求成功后,setTimeout等页面渲染完后,如果一页的数据过少,则要触发加载下一页
      onSuccess: (d) => {
        setTimeout(() => {
          // eslint-disable-next-line @typescript-eslint/no-use-before-define
          scrollMethod();
        });
        onSuccess?.(d);
      },
      onError: (e) => onError?.(e),
    },
  );

其实这里的代码很简单,调用useRequest

  • 传入useRequest需要的请求服务,在服务里,执行useInfiniteScroll的service服务获取新数据,再和旧数据进行合并。
  • useRequest钩子里执行useInfiniteScroll的钩子,设置loadingMore状态。
  • 请求成功后,看看能发触发滚动到底的逻辑(在一页数据撑不满一屏的情况下)。
// 执行加载下一页
  const loadMore = () => {
    if (noMore) return;
    setLoadingMore(true);
    run(finalData);
  };
// 重新加载的依赖变化时,重新加载
 useUpdateEffect(() => {
    run();
 }, [...reloadDeps]);

3、执行下一页和重新加载

至于执行下一页,其实就是执行run方法,并且把旧的列表数据传进去。

// 不传入旧数据,则是重新加载
  const reload = () => run();

如果不传旧数据就是重新加载,因为不会和旧数据进行合并。

4、滚动加载数据

// 判断目标元素是否滚动到底部,然后加载
  const scrollMethod = () => {
    ...
​
    if (scrollHeight - scrollTop <= clientHeight + threshold) {
      loadMore();
    }
  };
  
  // 滚动事件监听,useEventListener封装了移除监听的逻辑
  useEventListener(
    'scroll',
    () => {
      if (loading || loadingMore) {
        return;
      }
      scrollMethod();
    },
    { target },
  );

5、返回参数

return {
    data: finalData,
    loading: !loadingMore && loading,
    loadingMore,
    noMore,

    // 返回缓存后的方法引用
    loadMore: useMemoizedFn(loadMore),
    loadMoreAsync: useMemoizedFn(loadMoreAsync),
    reload: useMemoizedFn(reload),
    reloadAsync: useMemoizedFn(reloadAsync),
    mutate: setFinalData,
    cancel,
  };

返回数据、loading状态,加载下一页,重新加载等方法。值得注意的是,这里返回的方法都是经过缓存引用的方法。