React 之觸底加載
前言
我們經常在頁面開發中遇到 渲染列表 的情況,一般情有 切換分頁 和 無限追加 兩種模式,無限追加的情況一般需要借助觸底的鉤子(回調函數)來完成。如果是小程序,會有特殊的觸底鉤子(生命周期)。但是如果是非移動端,就需要我們自己實現判斷是否觸底的功能。今天給各位小伙伴帶來 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 ( <> {/* 省略其他代碼... */}
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 ( <> {/* 省略其他代碼... */}
讓我們來看下效果先:
功能雖然實現了,但是還需要繼續優化。
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 (
// 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 ( <> {/* 省略其他代碼... */}
6. 封裝「空狀態組件」
對于列表,我們一般需要自定義一個 空狀態 組件來缺省占位。
// src/components/Empty/index.jsx // 空狀態組件 import styles from './index.less'; import emptyList from '@/assets/images/common/empty-list.svg'; const Empty = (() => { return (
// 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 ( <> {/* 省略其他代碼... */}
當列表為空時的效果如下圖:
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 (
// 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 (
// 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 ( <> {/* 省略其他代碼... */}
小結
上述代碼是 React 中觸底加載的一種實現方式,可能并非最優解決方案。不過我們在此案例中使用了自定義 hook ,封裝了 加載更多組件 和 空狀態組件 ,也算是有一些其他的收獲。我們只有不斷地積累各種各樣的功能實現方案,才能真正具備獨立開發大型項目的能力。只有不斷積累,才能不斷成長!
React web前端
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。