首页 前端 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

-->

相关推荐

html5页面转vue?

html5页面转vue?

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

React.js 2023.12.01 0 50

angularj数据绑定占位符?

angularj数据绑定占位符?

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

React.js 2023.12.01 0 32

js版本信息查看?

js版本信息查看?

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

React.js 2023.11.30 0 30

前端(前端开发的就业现状及前景)

前端(前端开发的就业现状及前景)

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

React.js 2023.11.30 0 20

html简单网页框架代码?

html简单网页框架代码?

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

React.js 2023.11.30 0 38

支付宝
微信
赞助本站