背景
因为需要进行排序,antd 的 Table 组件,排序不能过页,所以直接不分页。
列表展示人数近千条,初次加载卡顿,觉得 dom 太多,渲染费时间,决定用虚拟列表
解决方式
- 自行实现 virtualList 组件,花挺多时间,选用第 2 种
- 使用 react-virtualized 组件库,由于该库没有拖拽功能,自行添加 sortable.js 实现拖拽功能,由于 sortable.js 自定义排序不太好实现,于是另找方法,react-sortable-hoc 有基于 react-virtualized 实现虚拟列表
- 最后采用 react-sortable-hoc 和 react-virtualized 结合的方式实现这一功能,解决了卡顿的问题
虚拟列表实现原理
虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。
- 计算当前可视区域起始数据索引(startIndex)
- 计算当前可视区域结束数据索引(endIndex)
- 计算当前可视区域的数据,并渲染到页面中
- 计算startIndex对应的数据在整个列表中的偏移位置startOffset并设置到列表上
由于只是对可视区域内的列表项进行渲染,所以为了保持列表容器的高度并可正常的触发滚动,将Html结构设计成如下结构:
<div class="infinite-list-container">
<div class="infinite-list-phantom"></div>
<div class="infinite-list">
<!-- item-1 -->
<!-- item-2 -->
<!-- ...... -->
<!-- item-n -->
</div>
</div>
- infinite-list-container 为可视区域的容器
- infinite-list-phantom 为容器内的占位,高度为总列表高度,用于形成滚动条
- infinite-list 为列表项的渲染区域
接着,监听 infinite-list-container 的 scroll 事件,获取滚动位置 scrollTop
- 假定可视区域高度固定,称之为 screenHeight
- 假定列表每项高度固定,称之为 itemSize
- 假定列表数据称之为 listData
- 假定当前滚动位置称之为 scrollTop
则可推算出:
- 列表总高度
listHeight = listData.length * itemSize
- 可显示的列表项数
visibleCount = Math.ceil(screenHeight / itemSize)
- 数据的起始索引
startIndex = Math.floor(scrollTop / itemSize)
- 数据的结束索引
endIndex = startIndex + visibleCount
- 列表显示数据为
visibleData = listData.slice(startIndex,endIndex)
当滚动后,由于渲染区域相对于可视区域已经发生了偏移,此时我需要获取一个偏移量startOffset
,通过样式控制将渲染区域偏移至可视区域中。
- 偏移量
startOffset = scrollTop - (scrollTop % itemSize);
使用效果
react-virtualized 效果 与 直接用 antd 效果相比
数据总数:200条
虚拟列表的 Tree
初次渲染 20 条数据,使用时间 15 ~ 25 ms
import { Column, Table } from 'react-virtualized';
import { useState, useLayoutEffect } from 'react'
import 'react-virtualized/styles.css';
const columns = [
{
dataIndex: "url",
title: '相貌'
},
{
dataIndex: "name",
title: '名称'
},
{
dataIndex: "class",
title: '班级'
},
{
dataIndex: "age",
title: '年龄'
},
{
dataIndex: "grade",
title: '成绩'
}
]
let time = 0;
function VirtualTable(props) {
const [data, setData] = useState([]);
useLayoutEffect(() => {
setData(mockData); // mockData 为数组,有 200 条数据
time = new Date().valueOf();
}, [])
useLayoutEffect(() => {
if (data.length !== 0) {
console.log(new Date().valueOf() - time);
}
}, [data])
return (
<Table
width={300}
height={300}
headerHeight={20}
rowHeight={30}
rowCount={data.length}
rowGetter={({ index }) => data[index]}
>
{
columns.map(item => <Column key={item.dataIndex} label={item.title} dataKey={item.dataIndex} width={250} />)
}
</Table>
)
}
使用 antd 的 Tree
初次渲染 200 条数据,使用时间 141 ~ 145ms
初次渲染 1000 条数据,使用时间 465 ~ 545ms
import { Table } from 'antd';
import { useState, useLayoutEffect } from 'react'
// antd 普通列表
let time = 0;
const columns = [
{
dataIndex: "url",
title: '相貌'
},
{
dataIndex: "name",
title: '名称'
},
{
dataIndex: "class",
title: '班级'
},
{
dataIndex: "age",
title: '年龄'
},
{
dataIndex: "grade",
title: '成绩'
}
]
function NormalTable(props) {
const [data, setData] = useState([]);
useLayoutEffect(() => {
setData(mockData);
time = new Date().valueOf();
}, [])
useLayoutEffect(() => {
if (data.length !== 0) {
console.log(new Date().valueOf() - time);
}
}, [data])
return (
<Table
columns={columns}
dataSource={data}
pagination={{
defaultPageSize: 200
}}
/>
)
}
React 自行实现虚拟列表
基于 onscroll 方法
外部传给虚拟列表组件:
- 列表每一项的高度 itemHeight
- 列表每一项的数据:listData
- 虚拟列表容器可视高度:screenHeight
实现虚拟列表的思路:
监听 infinite-list-container
的 onscoll ,获取已滚动高度 scrollTop
- 改变起始索引 start
- 结束索引 end 由 start 派生:
end = Math.floor(screenHeight / itemHeight) + start
infinite-list
的偏移量 translateY:-listHeight + scrollTop
- 列表总长度:
listHeight = listData.length * itemHeight
```jsx import React, { useState, useMemo, useRef, useEffect } from ‘react’; import ‘./index.css’.infinite-list-container {
overflow: auto;
}
- 列表总长度:
const VirtualList = (props) => { const { listData, itemHeight, screenHeight, } = props
const containerRef = useRef() // 列表总高度 const listHeight = useMemo(() => { return listData.length * itemHeight }, [listData])
// 起始索引 const [start, setStart] = useState(0)
// 结束索引 const end = useMemo(() => { return Math.floor(screenHeight / itemHeight) + start }, [start])
const [transform, setTransform] = useState(‘’)
useEffect(() => {
const { current: listContainer } = containerRef
setTransform(translateY(${-listHeight + listContainer.scrollTop}px)
)
}, [start])
useEffect(() => { const { current: listContainer } = containerRef if (listContainer) { listContainer.onscroll = function () { // 修改起始索引 setStart(Math.floor(listContainer.scrollTop / itemHeight)) } } }, [])
return (
export default VirtualList;
```jsx
const listData = []
for (let i = 0; i < 300; i++) {
listData.push({ node: i })
}
<VirtualList
listData={listData}
itemHeight={30}
screenHeight={500}
/>
基于 IntersectionObserver 方法
参考资料
「前端进阶」高性能渲染十万条数据(虚拟列表)
react-virtualized的API文档
基于 react-virtualized 的 react-sortable-hoc 实现虚拟列表
sortable.js 的 github 仓库