React 之触底加载

网友投稿 1103 2022-05-29

前言

我们经常在页面开发中遇到 渲染列表 的情况,一般情有 切换分页 和 无限追加 两种模式,无限追加的情况一般需要借助触底的钩子(回调函数)来完成。如果是小程序,会有特殊的触底钩子(生命周期)。但是如果是非移动端,就需要我们自己实现判断是否触底的功能。今天给各位小伙伴带来 React 中触底加载的一种实现方式(注:以下将使用函数式组件),希望能对各位有所帮助,蟹蟹٩(‘ω’)و。

一、业务场景

有一个列表页,需要在页面触底时,追加下一页数据到页面中。

二、实现思路

封装一个函数用于监听页面是否触底。

触底时请求下一页数据,并追加到要渲染的数组中。

进一步优化代码。

三、进行编码

1. 请求数据

首先我们需要把请求数据的方法写好,此处使用 ahooks 的 useRequest 。(因为脚手架 UmiJS 内置了 ahooks 的这个函数,所以下面从 umi 引入该函数。)

UmiJS 文档

ahooks 文档

全局工具函数 utils/index.js

// utils/index.js /** * 希望获得数组 * 如果传入的是数组则直接返回,否则返回一个空数组 * @param data 必填 传入的待处理数据 * @returns Array */ export const wantArray = (data) => (Array.isArray(data) ? data : []);

封装请求的 service.js

// service.js import { request } from 'umi'; // 公告列表 export function getNoticeList(params) { // xxx 为请求地址 return request('xxx', { params }); };

公告列表的 jsx 文件的关键代码,已加注释,可放心食用。

// 公告列表 jsx // 以下为关键代码 import { useEffect, useState } from 'react'; import { useRequest } from 'umi'; import { Card, Spin } from 'antd'; import ArticleItem from '@/components/ArticleItem';// 文章条目组件 import { wantArray } from '@/utils'; import { getNoticeList } from './service'; export default () => { /* ======================== 公告列表 ======================== */ const pageSize = 10;// 每一页条数,因需求不需要更改此项故用 const 定义 const [current, setCurrent] = useState(1);// 当前页码 const [list, setList] = useState([]);// 列表数组 // todo 请求数据 const { run: runGetNoticeList, loading: noticeListLoading } = useRequest(getNoticeList, { manual: true,// 开启手动请求 formatResult: res => {// 格式化数据 setCurrent(res?.current);// 设置 当前页码 setList([...list, ...wantArray(res?.data)]);// 追加数组 } }); // 进入页面后默认请求一次数据 useEffect(() => { runGetNoticeList({ current, pageSize }) }, []); // 省略其他代码... return ( <> {/* 省略其他代码... */} { list.map(value => { return ( ); }) } ); };

2. 是否触底

我们需要实现判断是否触底的函数,并对其进行节流处理。最后用 addEventListener 进行侦听(需要在组件销毁时销毁该侦听器)。

// 公告列表 jsx // 以下为关键代码 import { useEffect } from "react"; import { message } from "antd"; import { throttle } from "lodash"; export default () => { /** * 加载更多 * 此函数内进行接口请求等操作 */ const handleLoadMore = () => { // 为测试效果临时使用 message message.info("触底了~"); }; /** * 判断是否触底 * 此函数进行判断是否触底 * @param handler 必填 判断后执行的回调函数 * @returns null */ const isTouchBottom = (handler) => { // 文档显示区域高度 const showHeight = window.innerHeight; // 网页卷曲高度 const scrollTopHeight = document.body.scrollTop || document.documentElement.scrollTop; // 所有内容高度 const allHeight = document.body.scrollHeight; // (所有内容高度 = 文档显示区域高度 + 网页卷曲高度) 时即为触底 if (allHeight <= showHeight + scrollTopHeight) { handler(); }; }; /** * 节流 判断是否触底 * 将是否触底函数进行 节流 处理 * @returns function */ const useFn = throttle(() => { // 此处调用 加载更多函数 isTouchBottom(handleLoadMore); }, 500); useEffect(() => { // 开启侦听器,监听页面滚动 window.addEventListener("scroll", useFn); // 组件销毁时移除侦听器 return () => { window.removeEventListener("scroll", useFn) }; }, []); // 省略其他代码... };

让我们来看下效果先:

效果还行,那我们接着进行下一步。

3. 触底加载

我们只需在触底时进行数据请求即可。在此处有一个问题,即函数式组件中侦听器无法拿到实时更新的变量。需要借助 useRef 来进行辅助。这也是在开发过程中遇到的问题之一,当时是阅读了这篇文章 《React监听事件执行的方法中如何获取最新的state》 才得以解决。

// 公告列表 jsx // 以下为关键代码 import { useEffect, useState, useRef } from 'react'; import { useRequest } from 'umi'; import { Card, Spin, message } from 'antd'; import ArticleItem from '@/components/ArticleItem';// 文章条目组件 import { throttle } from 'lodash'; import { wantArray } from '@/utils'; import { getNoticeList } from './service'; export default () => { /* ======================== 公告列表 ======================== */ const pageSize = 10; const [current, setCurrent] = useState(1); const [list, setList] = useState([]); // 此处增加了一个变量用于保存 是否还有更多数据 const [isMore, setIsMore] = useState(true); // todo 请求数据 const { run: runGetNoticeList, loading: noticeListLoading } = useRequest(getNoticeList, { manual: true, formatResult: res => { setCurrent(res?.current);// 设置 当前页码 setList([...list, ...wantArray(res?.data)]);// 追加数组 // 如果当前页码大于等于总页数则设置 是否还有更多数据 为 false if (current >= Math.round(res.total / pageSize)) { setIsMore(false) }; } }); // 进入页面后默认请求一次数据 useEffect(() => { runGetNoticeList({ current, pageSize }) }, []); // 为解决监听函数无法获取到最新 state 值的问题,使用 useRef 代替 state const currentRef = useRef(null); useEffect(() => { currentRef.current = current }, [current]); const loadingRef = useRef(null); useEffect(() => { loadingRef.current = noticeListLoading }, [noticeListLoading]); const isMoreRef = useRef(null); useEffect(() => { isMoreRef.current = isMore }, [isMore]); // todo 加载更多 const handleLoadMore = () => { if (!loadingRef.current && isMoreRef.current) { message.info('加载下一页~'); // 防止 (current + 1) 更新不及时,创建一个临时变量 const temp = currentRef.current + 1; setCurrent(temp); runGetNoticeList({ current: temp, pageSize }); }; }; // todo 判断是否触底 const isTouchBottom = (handler) => { // 文档显示区域高度 const showHeight = window.innerHeight; // 网页卷曲高度 const scrollTopHeight = document.body.scrollTop || document.documentElement.scrollTop; // 所有内容高度 const allHeight = document.body.scrollHeight; // (所有内容高度 = 文档显示区域高度 + 网页卷曲高度) 时即为触底 if (allHeight <= showHeight + scrollTopHeight) { handler(); }; }; const useFn = throttle(() => { // 此处调用 加载更多函数 isTouchBottom(handleLoadMore); }, 500); useEffect(() => { // 开启侦听器,监听页面滚动 window.addEventListener("scroll", useFn); // 组件销毁时移除侦听器 return () => { window.removeEventListener("scroll", useFn) }; }, []); return ( <> {/* 省略其他代码... */} { list.map(value => { return ( ); }) } ); };

让我们来看下效果先:

功能虽然实现了,但是还需要继续优化。

4. 封装「触底加载hook」

如果多个页面都用到了这个触底加载的功能,就需要进行封装,因为这是一段代码,且不含 UI 部分,所以封装成一个 hook 。在 src 目录下新建文件夹并命名为 hooks ,然后新建文件夹 useTouchBottom 并在其之中新建 index.js 。

// src/hooks/useTouchBottom/index.js // 触底加载 hook import { useEffect } from 'react'; import { throttle } from 'lodash'; const isTouchBottom = (handler) => { // 文档显示区域高度 const showHeight = window.innerHeight; // 网页卷曲高度 const scrollTopHeight = document.body.scrollTop || document.documentElement.scrollTop; // 所有内容高度 const allHeight = document.body.scrollHeight; // (所有内容高度 = 文档显示区域高度 + 网页卷曲高度) 时即为触底 if (allHeight <= showHeight + scrollTopHeight) { handler(); } }; const useTouchBottom = (fn) => { const useFn = throttle(() => { if (typeof fn === 'function') { isTouchBottom(fn); }; }, 500); useEffect(() => { window.addEventListener('scroll', useFn); return () => { window.removeEventListener('scroll', useFn); }; }, []); }; export default useTouchBottom;

// 公告列表 jsx // 以下为关键代码 import { useEffect, useState, useRef } from 'react'; import { useRequest } from 'umi'; import { Card, Spin, message } from 'antd'; import ArticleItem from '@/components/ArticleItem';// 文章条目组件 import useTouchBottom from '@/hooks/useTouchBottom';// 触底加载 hook import { wantArray } from '@/utils'; import { getNoticeList } from './service'; export default () => { /* ======================== 公告列表 ======================== */ // ... // 为解决监听函数无法获取到最新 state 值的问题,使用 useRef 代替 state // ... // todo 加载更多 const handleLoadMore = () => { if (!loadingRef.current && isMoreRef.current) { message.info('加载下一页~'); const temp = currentRef.current + 1; setCurrent(temp); runGetNoticeList({ current: temp, pageSize }); }; }; // 使用 触底加载 hook useTouchBottom(handleLoadMore); // 省略其他代码... };

5. 封装「加载更多组件」

我们可以发现在加载的时候有些生硬,需要一个 加载更多 组件来救场,此处是一个最简易的版本。

// src/components/LoadMore/index.jsx // 加载更多组件 import styles from './index.less'; /** * @param status 状态 loadmore | loading | nomore * @param hidden 是否隐藏 */ const LoadMore = ({ status = 'loadmore', hidden = false }) => { return (

); }; export default LoadMore;

// src/components/LoadMore/index.less // 加载更多组件 .loadmore { padding: 12px 0; width: 100%; color: rgba(0, 0, 0, 0.6); font-size: 14px; text-align: center; }

// 公告列表 jsx // 以下为关键代码 import { useEffect, useState, useRef } from 'react'; import { useRequest } from 'umi'; import { Card, Spin, message } from 'antd'; import ArticleItem from '@/components/ArticleItem';// 文章条目组件 import LoadMore from '@/components/LoadMore'; // 加载更多组件 import useTouchBottom from '@/hooks/useTouchBottom';// 触底加载 hook import { wantArray } from '@/utils'; import { getNoticeList } from './service'; export default () => { /* ======================== 公告列表 ======================== */ // 新建一个变量用来保存 加载更多组件 状态,初始值为 loadmore const [loadMoreStatus, setLoadMoreStatus] = useState('loadmore'); // ... // 为解决监听函数无法获取到最新 state 值的问题,使用 useRef 代替 state const currentRef = useRef(null); useEffect(() => { currentRef.current = current }, [current]); // loading 和 isMore 变化时需要修改 loadMoreStatus 的状态 const loadingRef = useRef(null); useEffect(() => { loadingRef.current = noticeListLoading; if (noticeListLoading) { setLoadMoreStatus('loading') }; }, [noticeListLoading]); const isMoreRef = useRef(null); useEffect(() => { if (!isMore) { setLoadMoreStatus('nomore') }; isMoreRef.current = isMore; }, [isMore]); // 省略其他代码... return ( <> {/* 省略其他代码... */} { list.map(value => { return ( ); }) } {/* 加载更多组件 */} ); };

6. 封装「空状态组件」

对于列表,我们一般需要自定义一个 空状态 组件来缺省占位。

// src/components/Empty/index.jsx // 空状态组件 import styles from './index.less'; import emptyList from '@/assets/images/common/empty-list.svg'; const Empty = (() => { return (

暂无数据
暂无数据
); }); export default Empty;

// src/components/Empty/index.less // 空状态组件 .empty { padding: 50px 0; color: rgba(0, 0, 0, 0.65); font-size: 14px; text-align: center; img { margin-bottom: 16px; } }

// 公告列表 jsx // 以下为关键代码 import Empty from '@/components/Empty';// ? 空状态组件 export default () => { // 省略其他代码... return ( <> {/* 省略其他代码... */} {/* 空状态组件 */} {list.length === 0 && } { list.map(value => { return ( ); }) } ); };

当列表为空时的效果如下图:

7. 问题:页面缩放

经过测试,上述代码存在一个问题:当页面缩放时,判断是否触底的函数失效。经过排查,发现页面缩放时 网页卷曲高度 和 所有内容高度 会发生改变且等式 网页卷曲高度 + 网页卷曲高度 = 所有内容高度 不再成立。目前的一种解决方案是将判断改为 所有内容高度 <= 文档显示区域高度 + 网页卷曲高度 + 100 ,即:

// src/hooks/useTouchBottom/index.js // 触底加载 hook const isTouchBottom = (handler) => { // 文档显示区域高度 const showHeight = window.innerHeight; // 网页卷曲高度 const scrollTopHeight = document.body.scrollTop || document.documentElement.scrollTop; // 所有内容高度 const allHeight = document.body.scrollHeight; // (所有内容高度 = 文档显示区域高度 + 网页卷曲高度) 时即为触底 // 判断 所有内容高度 <= 文档显示区域高度 + 网页卷曲高度 + 100 if (allHeight <= showHeight + scrollTopHeight + 100) { handler(); } };

8. 完整代码

// utils/index.js /** * 希望获得数组 * 如果传入的是数组则直接返回,否则返回一个空数组 * @param data 必填 传入的待处理数据 * @returns Array */ export const wantArray = (data) => (Array.isArray(data) ? data : []);

// service.js import { request } from 'umi'; // 公告列表 export function getNoticeList(params) { // xxx 为请求地址 return request('xxx', { params }); };

// src/components/LoadMore/index.jsx // 加载更多组件 import styles from './index.less'; /** * @param status 状态 loadmore | loading | nomore * @param hidden 是否隐藏 */ const LoadMore = ({ status = 'loadmore', hidden = false }) => { return (

); }; export default LoadMore;

// src/components/LoadMore/index.less // 加载更多组件 .loadmore { padding: 12px 0; width: 100%; color: rgba(0, 0, 0, 0.6); font-size: 14px; text-align: center; }

// src/components/Empty/index.jsx // 空状态组件 import styles from './index.less'; import emptyList from '@/assets/images/common/empty-list.svg'; const Empty = (() => { return (

暂无数据
暂无数据
); }); export default Empty;

// src/components/Empty/index.less // 空状态组件 .empty { padding: 50px 0; color: rgba(0, 0, 0, 0.65); font-size: 14px; text-align: center; img { margin-bottom: 16px; } }

// 公告列表 jsx import { useEffect, useState, useRef } from 'react'; import { useRequest } from 'umi'; import { Card, Spin } from 'antd'; import ArticleItem from '@/components/ArticleItem';// 文章条目组件 import Empty from '@/components/Empty';// ? 空状态组件 import LoadMore from '@/components/LoadMore'; // 加载更多组件 import useTouchBottom from '@/hooks/useTouchBottom';// 触底加载 hook import { wantArray } from '@/utils'; import { getNoticeList } from './service'; const Notice = () => { /* ======================== 公告列表 ======================== */ const pageSize = 10; const [current, setCurrent] = useState(1); const [list, setList] = useState([]); const [isMore, setIsMore] = useState(true); const [loadMoreStatus, setLoadMoreStatus] = useState('loadmore'); // todo 请求数据 const { run: runGetNoticeList, loading: noticeListLoading } = useRequest(getNoticeList, { manual: true, formatResult: res => { setCurrent(res?.current); setList([...list, ...wantArray(res?.data)]); if (current >= Math.round(res.total / pageSize)) { setIsMore(false) }; } }); useEffect(() => { runGetNoticeList({ current, pageSize }) }, []); // 为解决监听函数无法获取到最新 state 值的问题,使用 useRef 代替 state const currentRef = useRef(null); useEffect(() => { currentRef.current = current }, [current]); const loadingRef = useRef(null); useEffect(() => { loadingRef.current = noticeListLoading; if (noticeListLoading) { setLoadMoreStatus('loading') }; }, [noticeListLoading]); const isMoreRef = useRef(null); useEffect(() => { if (!isMore) { setLoadMoreStatus('nomore') }; isMoreRef.current = isMore; }, [isMore]); // todo 加载更多 const handleLoadMore = () => { if (!loadingRef.current && isMoreRef.current) { const temp = currentRef.current + 1; setCurrent(temp); runGetNoticeList({ current: temp, pageSize }); }; }; useTouchBottom(handleLoadMore); return ( <> {/* 省略其他代码... */} {list.length === 0 && } { list.map(value => { return ( ); }) } ); }; export default Notice;

小结

上述代码是 React 中触底加载的一种实现方式,可能并非最优解决方案。不过我们在此案例中使用了自定义 hook ,封装了 加载更多组件 和 空状态组件 ,也算是有一些其他的收获。我们只有不断地积累各种各样的功能实现方案,才能真正具备独立开发大型项目的能力。只有不断积累,才能不断成长!

React web前端

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:共创共享共赢,华为云携手伙伴发布生态联合解决方案
下一篇:使用python完成mongodb数据库的增删改查
相关文章