草庐IT

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

gopal 2023-03-28 原文

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

简介

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

详细可看官网

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

实现原理

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

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

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

具体实现

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

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。

// 判断是否还有数据
const noMore = useMemo(() => {
  if (!isNoMore) return false;
  return isNoMore(finalData);
}, [finalData]);

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

// 通过 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 函数。

// 同步加载更多
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,进行重置:

useUpdateEffect(() => {
  run();
}, [...reloadDeps]);

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

// 滚动方法
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

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

scrollHeight

Element.scrollHeight 这个只读属性是一个元素内容高度的度量,包括由于溢出导致的视图中不可见内容。

clientHeight

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

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

有关大家都能看得懂的源码之ahooks useInfiniteScroll的更多相关文章

  1. UE4 源码阅读:从引擎启动到Receive Begin Play - 2

    一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame

  2. elasticsearch源码关于TransportSearchAction【阶段三】 - 2

    1.回顾.TransportServicepublicclassTransportServiceextendsAbstractLifecycleComponentTransportService:方法:1publicfinalTextendsTransportResponse>voidsendRequest(finalTransport.Connectionconnection,finalStringaction,finalTransportRequestrequest,finalTransportRequestOptionsoptions,TransportResponseHandlerT>

  3. (附源码)vue3.0+.NET6实现聊天室(实时聊天SignalR) - 2

    参考文章搭建文章gitte源码在线体验可以注册两个号来测试演示图:一.整体介绍  介绍SignalR一种通讯模型Hub(中心模型,或者叫集线器模型),调用这个模型写好的方法,去发送消息。  内容有:    ①:Hub模型的方法介绍    ②:服务器端代码介绍    ③:前端vue3安装并调用后端方法    ④:聊天室样例整体流程:1、进入网站->调用连接SignalR的方法2、与好友发送消息->调用SignalR的自定义方法 前端通过,signalR内置方法.invoke()  去请求接口3、监听接受方法(渲染消息)通过new signalR.HubConnectionBuilder().on

  4. Cesium源码解析一(terrain文件的加载、解析与渲染全过程梳理) - 2

    快速导航(持续更新中…)Cesium源码解析一(terrain文件的加载、解析与渲染全过程梳理)Cesium源码解析二(metadataAvailability的含义)Cesium源码解析三(metadata元数据拓展中行列号的分块规则解析)Cesium源码解析四(Quantized-Mesh(.terrain)格式文件在CesiumJS和UE中加载情况的对比)目录1.前言2.本篇的由来3.terrain文件的加载3.1更新环境3.2更新和执行渲染命令3.3数据优化3.4结束当前帧4.总结1.前言  目前市场上三维比较火的实现方案主要有两种,b/s的方案主要是Cesium,c/s的方案主要是u

  5. 停车系统源码-基于springboot+uniapp开源项目 - 2

    Iparking停车收费管理系统-可商用介绍Iparking是一款基于springBoot的停车收费管理系统,支持封闭车场和路边车场,支持微信支付宝多种支付渠道,支持多种硬件,涵盖了停车场管理系统的所有基础功能。技术栈Springboot,MybatisPlus,Beetl,Mysql,Redis,RabbitMQ,UniApp功能云端功能序号模块功能描述1系统管理菜单管理配置系统菜单2系统管理组织管理管理组织机构3系统管理角色管理配置系统角色,包含数据权限和功能权限配置4系统管理用户管理管理后台用户5系统管理租户管理多租户管理6系统管理公众号配置租户公众号配置7系统管理操作日志审计日志8系统

  6. 打通源码,高效定位代码问题|云效工程师指北 - 2

    大家好,我叫胡飞虎,花名虎仔,目前负责云效旗下产品Codeup代码托管的设计与开发。代码作为企业最核心的数据资产,除了被构建、部署之外还有更大的价值。为了帮助企业和团队挖掘更多源代码价值以赋能日常代码研发、运维等工作,云效代码团队在大数据和智能化方向进行了一系列的探索和实践(例如代码搜索与推荐),本文主要介绍我们如何通过直接打通源代码来提高研发与运维效率。随着微服务架构的流行,一个业务流程需要多个微服务共同完成。一旦出现问题,运维人员在面对数量多、调用链路复杂的情况下,很难快速锁定导致问题发生的罪魁祸首:代码。为了提高排查效率,目前常见的解决方案是:链路跟踪+日志分析工具相结合。即通过链路跟踪

  7. Android Studio开发之使用内容组件Content获取通讯信息讲解及实战(附源码 包括添加手机联系人和发短信) - 2

    运行有问题或需要源码请点赞关注收藏后评论区留言一、利用ContentResolver读写联系人在实际开发中,普通App很少会开放数据接口给其他应用访问。内容组件能够派上用场的情况往往是App想要访问系统应用的通讯数据,比如查看联系人,短信,通话记录等等,以及对这些通讯数据及逆行增删改查。首先要给AndroidMaifest.xml中添加响应的权限配置 下面是往手机通讯录添加联系人信息的例子效果如下分成三个步骤先查出联系人的基本信息,然后查询联系人号码,再查询联系人邮箱代码 ContactAddActivity类packagecom.example.chapter07;importandroid

  8. java 版本企业电子招投标采购系统源码之登录页面 - 2

    ​ 信息数智化招采系统服务框架:SpringCloud、SpringBoot2、Mybatis、OAuth2、Security前端架构:VUE、Uniapp、Layui、Bootstrap、H5、CSS3涉及技术:Eureka、Config、Zuul、OAuth2、Security、OSS、Turbine、Zipkin、Feign、Monitor、Stream、ElasticSearch等企业电子化采购系统企业电子化采购系统是明理公司在多家大、中、小型企业采购需求的分析与实际应用的基础上,结合企业采购流程优化再造理念开发的一体化电子招标采购平台,对于招标项目提供交易过程的全流程电子化、规范化管

  9. 自学5个月Java找到了9K的工作,我的方式值得大家借鉴 第一部分 - 2

    我是去年9月22日才正式学习Java的,因为在国营单位工作了4年,在天津一个月工资只有5000块,而且看不到任何晋升的希望,如果想要往上走,那背后就一定要有关系才行。而且国营单位的气氛是你干的多了,领导觉得你有野心,你干的不多,领导却觉得你这个人不错。我才26周岁,实在的受不了这种工作氛围,情绪已经压制了很多久,一心想着要跳出来,却一直找不到合适的机会。因为身边的朋友有在北京做Java开发的,他工作了四五年的时间,可以在北京拿到3万的月薪,说心里话我是真的羡慕,这远超出了我的认知范围。所以经过朋友的推荐,我开始学习Java,一共学了大概5个多月的时间,今年的3月6号在天津找到了一份Java开发

  10. 大家沉迷短视频无法自拔?Python爬虫进阶,带你玩转短视频 - 2

    大家好,我是辣条。现在短视频可谓是一骑绝尘,吃饭的时候、休息的时候、躺在床上都在刷短视频,今天给大家带来python爬虫进阶:美拍视频地址加密解析。短视频js逆向解析抓取目标工具使用重点学习内容项目思路解析抓取目标目标网址:美拍视频工具使用开发环境:win10、python3.7开发工具:pycharm、Chrome工具包:requests、xpath、base64重点学习内容爬虫采集数据的解析过程js代码调试技巧js逆向解析代码Python代码的转换项目思路解析进入到网站的首页挑选你感兴趣的分类根据首页地址获取到进入详情页面的超链接的跳转地址找到对应加密的视频播放地址数据这个数据是静态的网页

随机推荐