为什么做分页
从网络层面上看:
数据量过大,会造成报文长度过大,从而导致网络传输速度变慢
从渲染层面上看:
一次性插入过多DOM节点,会增加页面的渲染压力
怎么做分页
跟前端相关的分页逻辑主要可以分为以下几个阶段:
- 监听滚动
- 发送、接收请求
- 处理、更新数据
1、监听滚动
首先,我们需要设置 触发分页请求的条件:即 页面被滚动到底部
与之相关的几个物理量:
- document.documentElement.scrollTop:页面在竖直方向上滚动过的距离(scrollTop)
- document.documentElement. clientHeight:屏幕窗口的绝对高度(windowHeight)
- document.documentElement.scrollHeight:页面中所有内容的高度(包括由于溢出导致的视图中不可见的内容)(scrollHeight)
当满足以下条件时,可以判定页面滚动到了底部
scrollTop + windowHeight >= scrollHeight
复制代码
这部分的代码如下:
const THROTTLE_WAIT = 20; // 节流的等待间隔
useEffect(() => {
const listenScroll = throttle(() => {
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const windowHeight = document.documentElement.clientHeight || document.body.clientHeight;
const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
// 到达页面底部
if (scrollTop + windowHeight >= scrollHeight) {
getMoreData();
}
}, THROTTLE_WAIT);
window.addEventListener('scroll', listenScroll);
return () => {
window.removeEventListener('scroll', listenScroll);
}
}
}, [])
复制代码
2、发送、接收请求
分页请求的”分页“逻辑,需要前后端协同处理:
- 有序:主要依靠后端实现
- 不重复:前后端协同处理
之所以可能出现数据的重复,是因为:在两次请求之间可能会有新数据的插入
常见的解决方案如下:
- 方案1:前端请求第start到第start+size条数据,后端筛选出size条数据并返回,前端依靠Id做去重
- 方案2:前端发送当前页面上 最后一条数据的id,后端只返回 排在这条数据之后的数据,从而保证不重复
- 方案3:逆序显示,能保证不重复且数据完整,但违反直觉(最新的数据在列表最底部)
目前采用第一种方案
在发送请求这一阶段,要特别注意的是:避免短时间内重复发送请求
由上述代码可以发现,触及页面底部 会触发getMoreData方法的执行
由于接收请求/渲染数据需要一定的时间,所以需要限制这段时间内 不再发送分页请求
可以通过在redux中 维护一个布尔类型的loading变量:
- 发送请求时,将loading置为true
- 请求成功or失败,将loading置为false
在每一次发送分页请求之前,先进行条件判断:
const PAGE_SIZE = 10;
const needGetMoreData = (isEnd: boolean, loading: boolean) => !isEnd && !loading;
const getMoreData() = () => {
if (needGetMoreData(reduxState.data.isEnd, reduxState.loading)) {
// 发送分页请求
props.dispatch(getList({
start: reduxState.data.list.length,
size: PAGE_SIZE,
}))
}
}
复制代码
注意:
分页请求中,给参数start赋值时,必须使用当前渲染列表的长度
3、处理、更新数据
3.1、数据去重
我们希望将去重的逻辑封装成一个公用的方法,将标识字段 交给业务方去指定
思路如下:
- 维护一个哈希表hashMap,记录当前list中的所有id
- 遍历 新请求得到的list,拿出每个元素的id
- 若hashMap中存在id,就过滤掉这个元素
- 若hashMap中不存在这个id,就把这个元素加入列表中,并更新hashMap
代码如下:
function removeDuplicate<T>(
originList: T[]= [],
hashMap: Map<string, number>,
prop: keyof T
): T[] {
const newList: T[] = [];
originList.forEach((item) => {
const id = String(item[prop]);
if (hashMap.get(id) === undefined) {
newList.push(item);
hashMap.set(id, 1);
}
})
return newList;
}
复制代码
为了应对不同的业务场景:
- 定义泛型变量T,用于捕获列表的类型(在调用的时候传入)
- 使用keyof关键字去定义prop的类型,用于限制prop必须是类型T中的一个字段名称
3.2、数组拼接
初始化redux的state:
const reduxState = {
loading: false;
data: {
list: [];
isEnd: false;
},
hashMap: new Map(),
}
复制代码
在reducer中 定义更新逻辑:
const UNIQUE_ID = 'orderId'; // 用于做数据去重的标识字段
function reducer(state = reduxState, action) {
const update = {};
let newList: List[];
switch (action.type) {
case GET_LIST_REQ:
update.loading = true;
break;
case GET_LIST_SUCCESS:
update.loading = false;
newList = removeDuplicates<List>(action.data?.list || [], state.hashMap, UNIQUE_ID);
update.data = action.data;
update.data.list = [...state.data.list, ...newList];
break;
case GET_LIST_FAIL:
update.loading = false;
break;
default:
break;
}
return {
...state, ...update,
};
}
复制代码
需要注意的地方:
(1)update语句的赋值顺序
请求成功时,我们先用action.data去覆盖update.data,再用解构运算符 做数组的拼接
目的是 确保数组list以外的数据能够全部更新(例如:isEnd变量)
(2)reducer返回值的书写
要把更新的数据放在后面:
return {
...state, ...update
}
复制代码
写在最后
鉴于笔者只是一名未毕业的学生,实践经验非常有限,有写的不对or不好的地方欢迎大家在评论区批评指正!
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END