先看效果
什么是虚拟列表
虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。假设有10万条记录需要同时渲染,我们屏幕的可见区域的高度为550px,而列表项的高度为55px,则此时我们在屏幕中最多只能看到10个列表项,那么在渲染的时候,我们只需加载可视区的那10条即可
为啥需要使用虚拟列表
虚拟列表可以解决一次性渲染数据量过大时,页面卡顿,(比如: table不分页并且一次性加载上万条复杂的数据)
import React, {
useRef,
useEffect,
useContext,
createContext,
useReducer,
useState,
useMemo
} from 'react'
import { throttle, noop } from 'lodash'
// ===============reducer ============== //
const initialState = {
// 行高默认值
rowHeight: 50,
// 当前scrollTop
curScrollTop: 0,
}
function reducer(state: any, action: any) {
switch (action.type) {
//渲染的列表tr改变
case 'changeTrs': {
const curScrollTop = action.curScrollTop
return {
...state,
curScrollTop,
}
}
default:
throw new Error()
}
}
// ===============context ============== //
const ScrollContext = createContext({
dispatch: noop,
renderLen: 1,
start: 0,
rowHeightMap: {},
rowHeightSum: [],
rowHeight: initialState.rowHeight,
})
// =============组件 =================== //
function VWrapper(props: any): JSX.Element {
const { children, ...restProps } = props
const { renderLen, start, rowHeightMap, rowHeightSum } = useContext(ScrollContext)
console.log(rowHeightMap, rowHeightSum, renderLen, start)
const contents = children[1]
const tempNode = useMemo(() => {
if (Array.isArray(contents) && contents.length) {
return [children[0], contents.slice(start, start + renderLen)]
}
return children
}, [children, contents, renderLen, start])
return <tbody {...restProps}>{tempNode}</tbody>
}
function VRow(props: any, ref: any): JSX.Element {
const { children, style, ...restProps } = props
return (
<tr
{...restProps}
style={{
...style,
boxSizing: 'border-box'
}}
>
{children}
</tr>
)
}
function VCell(props: any): JSX.Element {
const { children, ...restProps } = props
return <td {...restProps}>{children}</td>
}
function VTable(props: any, otherParams: any): JSX.Element {
const { style, children, ...rest } = props
const { width, ...rest_style } = style
const { rowKeyList } = otherParams ?? {}
const [state, dispatch] = useReducer(reducer, initialState)
//行索引与行高对应的map初始值
const originData: any = {}
rowKeyList?.map((cv: string, index: number) => {
originData[index] = state.rowHeight
return cv
})
//记录行高累加高度
const rowHeightSum: number[] = []
//行索引与行高对应的map
const [rowHeightMap, setRowHeightMap] = useState(originData)
let currowHeightSum = 0
rowKeyList.map((cv: string, index: number) => {
currowHeightSum += rowHeightMap[index]
rowHeightSum.push(currowHeightSum)
return cv
})
//二分查找:第一个累加高度大于scrollTop的数据索引
const binarySearch = (array: number[], value: number) => {
let left = 0
let right = array.length - 1
while (left <= right) {
const middle = left + Math.floor((right - left) / 2)
if (array[middle] >= value) right = middle - 1
else left = middle + 1
}
return left
}
// 渲染的第一条数据的索引
let start = 0
start = binarySearch(rowHeightSum, state.curScrollTop)
//初始渲染条数
const [renderLen, setDefaultRenderLen] = useState(0)
const wrap_tableRef = useRef<HTMLDivElement>(null)
const tableRef = useRef<HTMLTableElement>(null)
//计算当前屏幕能展示数据条数
useEffect(() => {
const parentHeight = (wrap_tableRef.current?.parentNode as HTMLElement)?.offsetHeight
setDefaultRenderLen(Math.ceil(parentHeight / state.rowHeight))
}, [state.rowHeight])
// 偏移量,向上滚动的动画效果
let offsetStart = rowHeightSum[start]
? rowHeightMap[start] - (rowHeightSum[start] - state.curScrollTop)
: 0
const result = useMemo(() => rowKeyList.length - renderLen - start, [
renderLen,
rowKeyList.length,
start
])
//滚动到最底部时,偏移量重置为0
if (result <= 0) {
offsetStart = 0
}
// virtuallist总高度
const tableHeight = rowHeightSum[rowKeyList.length - 1] || state.rowHeight * rowKeyList.length
useEffect(() => {
const curRowHeightMap: any = {}
let n = 1
while (n <= renderLen) {
const index = rowKeyList.indexOf(
tableRef.current?.getElementsByTagName('tr')[n]?.getAttribute('data-row-key')
)
if (index > -1) {
curRowHeightMap[index] = tableRef.current?.getElementsByTagName('tr')[n]?.offsetHeight
}
n += 1
}
setRowHeightMap((prevState: any) => {
return Object.assign(prevState, curRowHeightMap)
})
}, [renderLen, state.curScrollTop, rowKeyList])
useEffect(() => {
const throttleScroll = throttle(e => {
const scrollTop: number = e?.target?.scrollTop ?? 0
dispatch({
type: 'changeTrs',
curScrollTop: scrollTop,
})
}, 16)
const ref = wrap_tableRef?.current?.parentNode as HTMLElement
if (ref) {
ref.addEventListener('scroll', throttleScroll, {
//防止调用事件监听器:为了提高滚动性能
passive: true,
})
}
return () => {
ref.removeEventListener('scroll', throttleScroll)
}
}, [wrap_tableRef])
return (
<div
className="virtuallist"
ref={wrap_tableRef}
style={{
width: '100%',
position: 'relative',
height: tableHeight,
boxSizing: 'border-box'
}}
>
<ScrollContext.Provider
value={{
dispatch,
rowHeight: state.rowHeight,
start,
rowHeightMap,
rowHeightSum,
renderLen,
}}
>
<table
{...rest}
ref={tableRef}
style={{
...rest_style,
width,
position: 'sticky',
top: 0,
transform: `translateY(-${offsetStart}px)`
}}
>
{children}
</table>
</ScrollContext.Provider>
</div>
)
}
const transformTable = (tempObj: any) => {
return (props: any) => VTable(props, tempObj)
}
// ================导出===================
export function VList(props: { rowKeyList?: string[] }): any {
const TableComponent = transformTable({
rowKeyList: props.rowKeyList,
})
return {
table: TableComponent,
body: {
wrapper: VWrapper,
row: VRow,
cell: VCell
}
}
}
.virtuallist .ant-table-tbody > tr > td > div {
box-sizing: border-box;
white-space: nowrap;
vertical-align: middle;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.virtuallist .ant-table-tbody > tr > td.ant-table-row-expand-icon-cell > div {
overflow: inherit;
}
.ant-table-bordered .virtuallist > table > .ant-table-tbody > tr > td {
border-right: 1px solid #f0f0f0;
}
import React, { useMemo } from "react";
import { Table, Tag } from "antd";
import { VList, } from "../../src/index";
import 'antd/dist/antd.css';
const generateData = () => {
let tempDataSource = [];
for (let i = 0; i < 100000; i++) {
tempDataSource.push({
uuid: `${i}`,
company_name1: i % 3 !== 1 ? `牛${i} 翠微股份给点力` : <> <Tag >翠微股份给点力翠微股份给点力</Tag><Tag >翠微股份给点力翠微股份给点力</Tag><Tag >翠微股份给点力翠微股份给点力</Tag></>,
company_name2: `牛${i} company index`,
company_name3: `牛${i} company company`,
company_name4: `牛${i} company company`,
});
}
return tempDataSource;
};
function SinglePageLoading() {
const dataSource = generateData()
const loading = false
const columns: any = [
{
title: '序号',
dataIndex: "uuid",
fixed: 'left',
width: 100,
},
{
title: "股1",
dataIndex: "company_name1",
width: 200
},
{
title: "股2",
dataIndex: "company_name2",
width: 200
},
{
title: "股3",
dataIndex: "company_name3",
width: 200
}
];
const components = useMemo(() => {
return VList({
rowKeyList: dataSource.map(cv => cv.uuid)
})
}, [dataSource])
return (
<>
<Table
columns={columns}
dataSource={dataSource}
pagination={false}
loading={loading}
scroll={{ y: 500, x: '100%' }}
rowKey={"uuid"}
components={components}
/>
</>
);
}
export default SinglePageLoading
ok,完事~