首页 前端 React.js 正文

React之触底加载实现方式

代码触底要加载,人生触底要反弹。

前言

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

一、业务场景

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

React之触底加载实现方式  第1张

二、实现思路

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

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

  • 进一步优化代码。

三、进行编码

3.1 请求数据

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

全局工具函数 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 (
        <>
            {/* 省略其他代码... */}

            <Card tabList={[{ key: '', tab: '公告' }]}>
                <Spin size="large" spinning={noticeListLoading} tip="加载中...">
                    {
                        list.map(value => {
                            return (
                                <ArticleItem
                                    key={value?.id}
                                    item={value}
                                />
                            );
                        })
                    }
                </Spin>
            </Card>
        </>
    );
};

3.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) };
    }, []);

    // 省略其他代码...
};

让我们来看下效果先:

React之触底加载实现方式  第2张

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

3.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 (
        <>
            {/* 省略其他代码... */}

            <Card tabList={[{ key: '', tab: '公告' }]}>
                <Spin size="large" spinning={noticeListLoading} tip="加载中...">
                    {
                        list.map(value => {
                            return (
                                <ArticleItem
                                    key={value?.id}
                                    item={value}
                                />
                            );
                        })
                    }
                </Spin>
            </Card>
        </>
    );
};

让我们来看下效果先:

React之触底加载实现方式  第3张

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

3.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);

    // 省略其他代码...
};

3.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 (
    <div className={styles.loadmore} hidden={hidden}>
      {status === 'loadmore' && <div>下拉加载</div>}
      {status === 'loading' && <div>加载中...</div>}
      {status === 'nomore' && <div>已加载全部内容</div>}
    </div>
  );
};

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 (
        <>
            {/* 省略其他代码... */}

            <Card tabList={[{ key: '', tab: '公告' }]}>
                <Spin size="large" spinning={noticeListLoading} tip="加载中...">
                    {
                        list.map(value => {
                            return (
                                <ArticleItem
                                    key={value?.id}
                                    item={value}
                                />
                            );
                        })
                    }

                    {/* 加载更多组件 */}
                    <LoadMore status={loadMoreStatus} hidden={list.length === 0} />
                </Spin>
            </Card>
        </>
    );
};

React之触底加载实现方式  第4张

3.6 封装 空状态组件

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

// service.js
import { request } from 'umi';

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

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

// 公告列表
export function getNoticeList(params) {
  // xxx 为请求地址
  return request('xxx', { params });
};2

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

React之触底加载实现方式  第5张

3.7 问题:页面缩放

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

// service.js
import { request } from 'umi';

// 公告列表
export function getNoticeList(params) {
  // xxx 为请求地址
  return request('xxx', { params });
};3

3.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 (
    <div className={styles.loadmore} hidden={hidden}>
      {status === 'loadmore' && <div>下拉加载</div>}
      {status === 'loading' && <div>加载中...</div>}
      {status === 'nomore' && <div>已加载全部内容</div>}
    </div>
  );
};

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;
}
// service.js
import { request } from 'umi';

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

// 公告列表
export function getNoticeList(params) {
  // xxx 为请求地址
  return request('xxx', { params });
};1
// 公告列表 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 (
        <>
            {/* 省略其他代码... */}

            <Card tabList={[{ key: '', tab: '公告' }]}>
                <Spin size="large" spinning={noticeListLoading} tip="加载中...">
                    {
                        list.map(value => {
                            return (
                                <ArticleItem
                                    key={value?.id}
                                    item={value}
                                />
                            );
                        })
                    }
                </Spin>
            </Card>
        </>
    );
};0

React之触底加载实现方式  第6张

结语

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

打赏
海报

本文转载自互联网,旨在分享有价值的内容,文章如有侵权请联系删除,部分文章如未署名作者来源请联系我们及时备注,感谢您的支持。

转载请注明本文地址:https://www.shouxicto.com/article/1589.html

相关推荐

在 React 中为什么要用JSX?

大家好,我是爱吃鱼的桶哥Z。相信使用React开发的童鞋,在编写组件的过程中接触最多的就是JSX。那么为什么React要用JSX来编写组件呢?JSX的本质是什么?是不是只有React才能用JSX?针对...

React.js 2022.08.20 0 1080

useEffect 与 useLayoutEffect 有什么区别?

在使用React开发的过程中,尤其是使用Hook开发组件,我们需要与后端进行交互的时候,通常我们会在useEffect这个勾子函数中执行,它与class组件中的componentDidMount生命周...

React.js 2022.08.20 0 984

setState 是同步更新还是异步更新?

setState 是同步更新还是异步更新?

我们经常在页面开发中遇到 渲染列表 的情况,一般情有切换分页和无限追加两种模式,无限追加的情况一般需要借助触底的钩子(回调函数)来完成。...

React.js 2022.08.20 1 1027

如何提升 React 代码的可维护性?

如何提升 React 代码的可维护性?

我们经常在页面开发中遇到 渲染列表 的情况,一般情有切换分页和无限追加两种模式,无限追加的情况一般需要借助触底的钩子(回调函数)来完成。...

React.js 2022.08.20 0 1045

Agora Web UIKit:快速构建视频通话或直播

     Agora 每月平均为人们提供超过 500 亿分钟的视频社交支持,随着开源、社区驱动的 Web UIKit 的发布,比以往任何时候都更容易构建自定义视频聊天应用程序或启动实时流媒...

React.js 2022.06.02 0 880

发布评论

ainiaobaibaibaibaobaobeishangbishibizuichiguachijingchongjingdahaqiandaliandangaodw_dogedw_erhadw_miaodw_tuzidw_xiongmaodw_zhutouganbeigeiliguiguolaiguzhanghahahahashoushihaixiuhanheixianhenghorse2huaixiaohuatonghuaxinhufenjiayoujiyankeaikeliankouzhaokukuloukunkuxiaolandelinileimuliwulxhainiolxhlikelxhqiuguanzhulxhtouxiaolxhwahahalxhzanningwennonuokpinganqianqiaoqinqinquantouruoshayanshengbingshiwangshuaishuijiaosikaostar0star2star3taikaixintanshoutianpingtouxiaotuwabiweifengweiquweiwuweixiaowenhaowoshouwuxiangjixianhuaxiaoerbuyuxiaokuxiaoxinxinxinxinsuixixixuyeyinxianyinyueyouhenghengyuebingyueliangyunzanzhajizhongguozanzhoumazhuakuangzuohenghengzuoyi
支付宝
微信
赞助本站