原生+React实现懒加载(无限滚动)列表方式

应用场景

懒加载列表或叫做无限滚动列表,也是一种性能优化的方式,其可疑不必一次性请求所有数据,可以看做是分页的另一种实现形式,较多适用于移动端提升用户体验,新闻、资讯浏览等。

效果预览

思路剖析

  • 设置临界元素,当临界元素进入可视范围时请求并追加新数据。
  • 根据可视窗口和滚动元素组建的关系确定数据加载时机。
container.clientHeight - wrapper.scrollTop <= wrapper.clientHeight

原生代码实现

index.html

<body>
 <div id="wrapper" onscroll="handleScroll()">
 <ul id="container"></ul>
 </div>
 <script type="text/javascript" src="./index.js"></script>
</body>

index.css

* {
 margin: 0;
 padding: 0;
}
#wrapper {
 margin: 100px auto;
 width: 300px;
 height: 300px;
 border: 1px solid rgba(100, 100, 100, 0.2);
 overflow-y: scroll;
}

ul#container {
 list-style: none;
 padding: 0;
 width: 100%;
}
ul#container > li {
 height: 30px;
 width: 100%;
}
ul#container > li.green-item {
 background-color: #c5e3ff;
}
ul#container > li.red-item {
 background-color: #fff5d5;
}

index.js

// 模拟数据构造
const arr = [];
const nameArr = ['Alice', 'July', 'Roman', 'David', 'Sara', 'Lisa', 'Mike'];

let curPage = 1;
let noData = false;
const curPageSize = 20;
const getPageData = (page, pageSize) => {
 if (page > 5) return [];
 const arr = [];
 // const nameArr = ['Alice', 'July', 'Roman', 'David', 'Sara', 'Lisa', 'Mike'];
 for (let i = 0; i < pageSize; i++) {
 arr.push({
 number: i + (page - 1) * pageSize,
 name: `${nameArr[i % nameArr.length]}`,
 });
 }
 return arr;
};

const wrapper = document.getElementById('wrapper');
const container = document.getElementById('container');
let plainWrapper = null;

/**
 * @method handleScroll
 * @description: 滚动事件监听
 */
const handleScroll = () => {
 // 当临界元素进入可视范围时,加载下一页数据
 if (
 !noData &&
 container.clientHeight - wrapper.scrollTop <= wrapper.clientHeight
 ) {
 curPage++;
 console.log(curPage);
 const newData = getPageData(curPage, curPageSize);
 renderList(newData);
 }
};

/**
 * @description: 列表渲染
 * @param {Array} data
 */
const renderList = (data) => {
 // 没有更多数据时
 if (!data.length) {
 noData = true;
 plainWrapper.innerText = 'no more data...';
 return;
 }
 plainWrapper && container.removeChild(plainWrapper); //移除上一个临界元素
 const fragment = document.createDocumentFragment();
 data.forEach((item) => {
 const li = document.createElement('li');
 li.className = item.number % 2 === 0 ? 'green-item' : 'red-item'; //奇偶行元素不同色
 const text = document.createTextNode(
 `${`${item.number}`.padStart(7, '0')}-${item.name}`
 );
 li.appendChild(text);
 fragment.appendChild(li);
 });
 const plainNode = document.createElement('li');
 const text = document.createTextNode('scroll to load more...');
 plainNode.appendChild(text);
 plainWrapper = plainNode;
 fragment.appendChild(plainNode); //添加新的临界元素
 container.appendChild(fragment);
};

// 初始渲染
renderList(getPageData(curPage, curPageSize));

迁移到React

React 中实现时可以省去复杂的手动渲染逻辑部分,更关注数据。

store/data.ts

import { IDataItem } from '../interface';
const nameArr = ['Alice', 'July', 'Roman', 'David', 'Sara', 'Lisa', 'Mike'];

export const getPageData = (
 page: number = 1,
 pageSize: number = 10
): Array<IDataItem> => {
 if (page > 5) return [];
 const arr = [];
 // const nameArr = ['Alice', 'July', 'Roman', 'David', 'Sara', 'Lisa', 'Mike'];
 for (let i = 0; i < pageSize; i++) {
 arr.push({
 number: i + (page - 1) * pageSize,
 name: `${nameArr[i % nameArr.length]}`,
 });
 }
 return arr;
};

LazyList.tsx

/*
 * @Description: 懒加载列表(无限滚动列表)
 * @Date: 2021-12-20 15:12:15
 * @LastEditTime: 2021-12-20 16:04:18
 */
import React, { FC, useCallback, useEffect, useReducer, useRef } from 'react';
import { getPageData } from './store/data';
import { IDataItem } from './interface';

import styles from './index.module.css';

export interface IProps {
 curPageSize?: number;
}

export interface IState {
 curPage: number;
 noData: boolean;
 listData: Array<IDataItem>;
}
const LazyList: FC<IProps> = ({ curPageSize = 10 }: IProps) => {
 const clientRef: any = useRef(null);
 const scrollRef: any = useRef(null);

 const [state, dispatch] = useReducer(
 (state: IState, action: any): IState => {
 switch (action.type) {
 case 'APPEND':
 return {
 ...state,
 listData: [...state.listData, ...action.payload.listData],
 };
 default:
 return { ...state, ...action.payload };
 }
 },
 {
 curPage: 1,
 noData: false,
 listData: [],
 }
 );
 /**
 * @method handleScroll
 * @description: 滚动事件监听
 */
 const handleScroll = useCallback(() => {
 const { clientHeight: wrapperHeight } = scrollRef.current;
 const { scrollTop, clientHeight } = clientRef.current;

 // 当临界元素进入可视范围时,加载下一页数据
 if (!state.noData && wrapperHeight - scrollTop <= clientHeight) {
 console.log(state.curPage);
 const newData = getPageData(state.curPage, curPageSize);
 dispatch({
 type: 'APPEND',
 payload: { listData: newData },
 });
 dispatch({
 payload: {
 curPage: state.curPage + 1,
 noData: !(newData.length > 0),
 },
 });
 }
 }, [state.curPage, state.noData]);

 useEffect(() => {
 const newData = getPageData(1, curPageSize);
 dispatch({
 type: 'APPEND',
 payload: { listData: newData },
 });
 dispatch({
 payload: {
 curPage: 2,
 noData: !(newData.length > 0),
 },
 });
 }, []);

 return (
 <div className={styles[`wrapper`]} ref={clientRef} onScroll={handleScroll}>
 <ul className={styles[`container`]} ref={scrollRef}>
 {state.listData.map(({ number, name }) => (
 <li
 key={number}
 className={
 number % 2 === 0 ? styles[`green-item`] : styles[`red-item`]
 }
 >
 {number}-{name}
 </li>
 ))}
 {<li>{state.noData ? 'no more' : 'scroll'}</li>}
 </ul>
 </div>
 );
};
export default LazyList;

总结

作者:小天才程序员原文地址:https://wentao.blog.csdn.net/article/details/122041761

%s 个评论

要回复文章请先登录注册