前言
在前端开发过程中,有时候会遇到一些不能使用分页来加载数据的情况,因此当我们需要渲染上十万条数据的时候,可能会造成渲染的卡顿,导致用户体验特别不好,那么对于这种情况我们该怎么去解决呢?这个时候就不得不提到虚拟列表
什么是虚拟列表
虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。假设有10万条记录需要同时渲染,我们屏幕的可见区域的高度为550px,而列表项的高度为55px,则此时我们在屏幕中最多只能看到10个列表项,那么在渲染的时候,我们只需加载可视区的那10条即可
为啥需要使用虚拟列表
虚拟列表可以解决一次性渲染数据量过大时,页面卡顿,(比如: table不分页并且一次性加载上万条复杂的数据)
成熟的虚拟列表方案
Ant-design — 虚拟列表
但是这些成熟的方案不一定能满足你的需求(比如:antd官网这个案列不支持hover时候行高亮),因此,我们需要知道这个虚拟列表的实现原理,以便在以后的工作中,随机应变,举一反三
自己实现
实现步骤 外层容器高度固定(550),并且设置(overflowY: ‘auto’)
计算内层容器放下全部数据的高度(total * 55)
计算在这个固定容器高度下可视区可以装多少条数据(limit = 550 / 55)
滚动的时候,通过参数scrollTop,可以计算可视区展示的第一条数据是第几条(startIndex = scrollTop / 55 )和最后一条数据是第几条(endIndex = limit + startIndex)
优化,加一个缓冲条数bufferSize,防止鼠标滚动过快的时候出现空白区域
固定外层容器高度
<div style={{ margin: '0 auto', height: '550px', width: '100%', overflowY: 'auto', msOverflowX: 'hidden', // 为了直观点这里用红色表示外层容器高度 border:'1px solid red', }} onScroll={onScroll} ref={ref} > <div>{/* 内存容器 */}</div> </div>
向容器中添加一层内层容器,并计算在总数据下,内层容器被撑开的最大高度
<div style={{ margin: '0 auto', height: '550px', width: '100%', overflowY: 'auto', msOverflowX: 'hidden', border:'1px solid red', }} onScroll={onScroll} ref={ref} > <div style={{ height: `${total * 55}px`, position: 'relative' }}> {/* 大量的数据 */} </div> </div>
计算在外层容器高度范围内可见的数据条数
limit = 550(外层容器的height) / 55(每一条数据的行高) 1 计算可视区第一条和最后一条数据分别在总数据的第几条 const limit = 10; let originStartIdx = 0; // total是总共有多少条数据 const total = 100000; // rowHeight是每一条数据的行高 const rowHeight = 55; const vTable = () => { const [startIndex, setStart] = useState(Math.max(originStartIdx, 0)); const [endIndex, setEnd] = useState(Math.min(originStartIdx + limit, total - 1)); const onScroll = (e: any) => { if (e.target === ref.current) { const { scrollTop } = e.target; // scrollTop 获取到的是被滚动元素在垂直位置滚动的距离。 const currIndex = Math.floor(scrollTop / rowHeight; if (originStartIdx !== currIndex) { originStartIdx = currIndex; setStart(Math.max(currIndex, 0)) setEnd(Math.min(currIndex + limit, total - 1)) } } } }
这样滑动过快会出现空白区域
添加缓冲参数,防止滚动过快出现空白
const limit = 10; let originStartIdx = 0; // total是总共有多少条数据 const total = 100000; // rowHeight是每一条数据的行高 const rowHeight = 55; // 用于缓冲(防止滚动过快,出现白屏) const bufferSize = 20; const vTable = () => { const [startIndex, setStart] = useState(Math.max(originStartIdx - bufferSize, 0)); const [endIndex, setEnd] = useState(Math.min(originStartIdx + limit + bufferSize, total - 1)); const onScroll = (e: any) => { if (e.target === ref.current) { const { scrollTop } = e.target; // scrollTop 获取到的是被滚动元素在垂直位置滚动的距离。 const currIndex = Math.floor(scrollTop / rowHeight; if (originStartIdx !== currIndex) { originStartIdx = currIndex; setStart(Math.max(currIndex - bufferSize, 0)) setEnd(Math.min(currIndex + limit + bufferSize, total - 1)) } } } }
添加后滑动过快空白消失
全部代码
```go import { useState, useRef } from "react"; import { select } from "@querycap-ui/core"; const total = 100000; const rowHeight = 55; // 用于缓冲(防止滚动过快,出现白屏) const bufferSize = 20; const limit = 10; let originStartIdx = 0; const getdata = () => { const data = []; for (let i = 0; i < total; i++) { data.push({ title: `标题${i}`, age: `年纪${i}`, address: `地址${i}`, }) } return data; } const data = getdata(); const Home = () => { const [current, setCurrent] = useState<undefined | number>(); const [startIndex, setStart] = useState(Math.max(originStartIdx - bufferSize, 0)); const [endIndex, setEnd] = useState(Math.min(originStartIdx + limit + bufferSize, total - 1)); const ref = useRef<HTMLDivElement>(null); const onScroll = (e: any) => { if (e.target === ref.current) { const { scrollTop } = e.target; // scrollTop 获取到的是被滚动元素在垂直位置滚动的距离。 const currIndex = Math.floor(scrollTop / rowHeight); if (originStartIdx !== currIndex) { originStartIdx = currIndex; setStart(Math.max(currIndex - bufferSize, 0)) setEnd(Math.min(currIndex + limit + bufferSize, total - 1)) } } } const edite = (data: any, index: number) => { setCurrent(index); console.log('编辑了这一条数据---->',data) } const rowRenderer = (obj: any) => { const { index, style } = obj; return ( <div key={index} style={style} css={ select().backgroundColor(index === current ? '#dcebf8' : '').display('flex') .with(select(':hover').backgroundColor(index === current ? '#dcebf8' : '#fafafa')) .with(select('>*').flex(1).padding('0px 16px')) } > <span>{data[index].title}</span> <span>{data[index].age}</span> <span>{data[index].address}</span> <a onClick={() => edite(data[index], index)}>编辑</a> </div> ) } // 可视区域的数据 const pushData = () => { const content = []; for (let i = startIndex; i <= endIndex; ++i) { content.push( rowRenderer({ index: i, style: { height: `${rowHeight}px`, lineHeight: `${rowHeight}px`, left: 0, right: 0, position: "absolute", top: i * rowHeight, borderBottom: "1px solid #f0f0f0", width: "100%", cursor: 'pointer', } }) ); } return content; } return ( <div style={{ margin: '0 auto', height: '550px', width: '100%', overflowY: 'auto', msOverflowX: 'hidden', border:'1px solid red', }} onScroll={onScroll} ref={ref} > <div style={{ height: `${total * 55}px`, position: 'relative' }}> {pushData()} </div> </div> ); };