前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >大家都能看得懂的源码之ahooks useInfiniteScroll

大家都能看得懂的源码之ahooks useInfiniteScroll

作者头像
GopalFeng
发布2022-09-23 11:46:26
6110
发布2022-09-23 11:46:26
举报

本文是深入浅出 ahooks 源码系列文章的第十七篇,该系列已整理成文档-地址[1]。觉得还不错,给个 star[2] 支持一下哈,Thanks。

简介

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

详细可看官网[3]

注意:这里的无限滚动指的是常见的点击加载更多或者说下拉加载更加功能,而不是虚拟滚动,虚拟滚动后面会讲到。

实现原理

实现原理:使用了 useRequest[4] hook 负责进行请求后台数据。其中 reloadAsync 对应 useRequest 的 runAsync,reload 对应 useRequest 的 run。前者返回 Promise,需要自行处理异常。后者内部已经做了异常处理。

另外假如传入 target 和 isNoMore 参数,通过监听 scroll 事件,判断是否滚动到指定的位置(支持设置 threshold 值-距离底部距离阈值),进行自动发起加载更多请求,从而实现滚动自动加载效果。

大概说完原理,来看代码。

具体实现

入参以及状态定义,可以直接看注释:

代码语言:javascript
复制
const useInfiniteScroll = <TData extends Data>(
  // 请求服务
  service: Service<TData>,
  options: InfiniteScrollOptions<TData> = {},
) => {
  const {
    // 父级容器,如果存在,则在滚动到底部时,自动触发 loadMore。需要配合 isNoMore 使用,以便知道什么时候到最后一页了。
    target,
    // 是否有最后一页的判断逻辑,入参为当前聚合后的 data
    isNoMore,
    // 下拉自动加载,距离底部距离阈值
    threshold = 100,
    // 变化后,会自动触发 reload
    reloadDeps = [],
    // 默认 false。 即在初始化时自动执行 service。
    // 如果设置为 true,则需要手动调用 reload 或 reloadAsync 触发执行。
    manual,
    // service 执行前触发
    onBefore,
    // 执行后
    onSuccess,
    // service reject 时触发
    onError,
    // service 执行完成时触发
    onFinally,
  } = options;

  // 最终的数据
  const [finalData, setFinalData] = useState<TData>();
  // 是否loading more
  const [loadingMore, setLoadingMore] = useState(false);
  // 省略代码...
};

判断是否有数据:isNoMore 的入参是当前聚合后的 data。

代码语言:javascript
复制
// 判断是否还有数据
const noMore = useMemo(() => {
  if (!isNoMore) return false;
  return isNoMore(finalData);
}, [finalData]);

通过 useRequest 处理请求,可以看到 onBefore、onSuccess、onError、onFinally、manual 等参数都是直接传到了 useRequest 中。

代码语言:javascript
复制
// 通过 useRequest 处理请求
const { loading, run, runAsync, cancel } = useRequest(
  // 入参,将上次请求返回的数据整合到新的参数中
  async (lastData?: TData) => {
    const currentData = await service(lastData);
    // 首次请求,则直接设置
    if (!lastData) {
      setFinalData(currentData);
    } else {
      setFinalData({
        ...currentData,
        // service 返回的数据必须包含 list 数组,类型为 { list: any[], ...rest }
        // @ts-ignore
        list: [...lastData.list, ...currentData.list],
      });
    }
    return currentData;
  },
  {
    // 是否手动控制
    manual,
    // 请求结束
    onFinally: (_, d, e) => {
      // 设置 loading 为 false
      setLoadingMore(false);
      onFinally?.(d, e);
    },
    // 请求前
    onBefore: () => onBefore?.(),
    // 请求成功之后
    onSuccess: d => {
      setTimeout(() => {
        // eslint-disable-next-line @typescript-eslint/no-use-before-define
        scrollMethod();
      });
      onSuccess?.(d);
    },
    onError: e => onError?.(e),
  },
);

loadMore/loadMoreAsync 和 reload/reloadAsync 分别对应调用的是 useRequest 的 run 和 runAsync 函数。

代码语言:javascript
复制
// 同步加载更多
const loadMore = () => {
  // 假如没有更多,直接返回
  if (noMore) return;
  setLoadingMore(true);
  // 执行 useRequest
  run(finalData);
};

// 异步加载更多,返回的值是 Promise,需要自行处理异常
const loadMoreAsync = () => {
  if (noMore) return Promise.reject();
  setLoadingMore(true);
  return runAsync(finalData);
};

const reload = () => run();
const reloadAsync = () => runAsync();

并且当 reloadDeps 依赖发生变化的时候,会触发 reload,进行重置:

代码语言:javascript
复制
useUpdateEffect(() => {
  run();
}, [...reloadDeps]);

最后就是滚动自动加载的逻辑,通过 scrollHeight - scrollTop <= clientHeight + threshold 结果判断是否触底。

代码语言:javascript
复制
// 滚动方法
const scrollMethod = () => {
  const el = getTargetElement(target);
  if (!el) {
    return;
  }
  // Element.scrollTop 属性可以获取或设置一个元素的内容垂直滚动的像素数。
  const scrollTop = getScrollTop(el);
  // Element.scrollHeight 这个只读属性是一个元素内容高度的度量,包括由于溢出导致的视图中不可见内容。
  const scrollHeight = getScrollHeight(el);
  // 这个属性是只读属性,对于没有定义CSS或者内联布局盒子的元素为0,否则,它是元素内部的高度(单位像素),包含内边距,但不包括水平滚动条、边框和外边距。
  const clientHeight = getClientHeight(el);

  // 根据上面三个值以及 threshold 判断是否进行加载更多
  if (scrollHeight - scrollTop <= clientHeight + threshold) {
    loadMore();
  }
};

// 监听滚动事件
useEventListener(
  'scroll',
  () => {
    if (loading || loadingMore) {
      return;
    }
    scrollMethod();
  },
  { target },
);

上面提到的三个重要的值 scrollTop,scrollHeight,clientHeight 对应的值分别为以下结果:

scrollTop[5]

Element.scrollTop 属性可以获取或设置一个元素的内容垂直滚动的像素数。一个元素的 scrollTop 值是这个元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为 0。

scrollHeight[6]

Element.scrollTop 属性可以获取或设置一个元素的内容垂直滚动的像素数。一个元素的 scrollTop 值是这个元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为 0。

clientHeight[7]

这个属性是只读属性,对于没有定义 CSS 或者内联布局盒子的元素为 0,否则,它是元素内部的高度 (单位像素),包含内边距,但不包括水平滚动条、边框和外边距。clientHeight 可以通过 CSS height + CSS padding - 水平滚动条高度 (如果存在) 来计算。

本文已收录到个人博客[8]中,欢迎关注~

参考资料

[1]地址: https://gpingfeng.github.io/ahooks-analysis/

[2]star: https://github.com/GpingFeng/ahooks-analysis

[3]官网: https://ahooks.gitee.io/zh-CN/hooks/use-infinite-scroll

[4]useRequest: https://ahooks.gitee.io/zh-CN/hooks/use-request/index

[5]scrollTop: https://developer.mozilla.org/zh-CN/docs/Web/API/Element/scrollTop

[6]scrollHeight: https://developer.mozilla.org/zh-CN/docs/Web/API/Element/scrollHeight

[7]clientHeight: https://developer.mozilla.org/zh-CN/docs/Web/API/Element/clientHeight

[8]博客: https://github.com/GpingFeng/gopal-blog

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-08-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端杂货铺 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
  • 实现原理
  • 具体实现
    • 参考资料
    相关产品与服务
    容器服务
    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档