1. 应用场景
需要用列表的形式展示大量的数据,本文章只针对规则的、等高且固定高度的列表。如图:
前端渲染大量数据时会造成页面卡顿,原因之一是渲染的DOM节点太多,而虚拟列表只渲染可见区域的DOM节点,极大的优化了渲染性能。
2. 思路
初次加载时,只渲染初始的一部分数据,页面滚动时,动态计算需要展示的数据和滚动的位置。为此,DOM的设计需要三个区域----容器、列表展示区域、支撑滚动条区域
- 容器:包裹列表展示区和滚动条支撑区;
- 列表展示区域:真实渲染的列表项区域,也就是可见的列表项部分;
-
支撑滚动条区域:用于支撑容器的高度,使容器出现滚动条。
样式的命名可以个性化一点......
<div class="jisl-container">
<div class="phantom"></div>
<div class="view">
<!-- item-1 -->
<!-- item-2 -->
<!-- ...... -->
<!-- item-n -->
</div>
</div>
3. 代码实现
目录结构,新建一个文件夹,然后在文件夹中新建js和css文件
VirtualList\
index.js
style.css
在index.js中编辑代码
import React, {useState, useEffect, useRef} from 'react';
import './style.css';
const VirtualList = (props) => {
const scrollRef = useRef(); // 滚动条ref
const {
data, // 渲染的数据
count, // 列表的数量、长度
size, // 可视区渲染的列表项数量(真实DOM节点数量)
viewSize, // 可视区能看到的列表数量, 数值比size小, 即DOM比可见数量多, 具有缓冲作用
rowHeight, // 每一行列表项的高度
renderNode, // 渲染的列表项DOM节点
} = props;
const [startIndex, setStartIndex] = useState(0); // 起始索引
const [phantomHeight, setPhantomHeight] = useState(0); // 占位区的高度
const [startOffset, setStartOffset] = useState(0); // 渲染区域偏移量
// 计算支撑滚动条区域的高度
useEffect(() => {
setPhantomHeight(rowHeight * count);
}, [count, rowHeight])
/**
* 滚动时更新显示区域的数据和高度
* @param {DOM.event} e
*/
const onScroll = e => {
let scrollTop = e.target.scrollTop;
let offset = scrollTop - (scrollTop % rowHeight);
let index = Math.floor(scrollTop / rowHeight);
setStartOffset(offset);
setStartIndex(index);
}
return (
<div
className="jisl-container"
style={
(data && data.length > viewSize) || (data && data.length === 0)
? { height: rowHeight * viewSize }
: { height: rowHeight * data.length }
}
onScroll={onScroll}
>
<div className="phantom" style={{height: phantomHeight}} />
<div
className="view"
style={{transform: `translateY(${startOffset}px)`}}
>
{
data instanceof Array && data.length > 0
? data.slice(startIndex, startIndex + size).map((item, index) => {
if(Object.prototype.toString.call(renderNode) !== '[object Function]') return;
return renderNode(data, item, index + startIndex);
})
: <div />
}
</div>
</div>
)
}
export default VirtualList;
在style.css中编写样式
// 外层容器
.jisl-container {
position: relative;
width: 100%;
overflow-y: auto;
overflow-x: hidden;
background: #fff;
box-shadow: 0 2px 5px -2px rgba(0,0,0,.05),
0 4px 10px 0 rgba(0,0,0,.08),
0 6px 20px 4px rgba(0,0,0,.05);
}
// 支撑区域
.phantom {
width: 100%;
background: #fff;
}
// 可视区列表项
.view {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
background: #fff;
}
- 外层容器设置overflow,只展示可见区域,并且position设置relative。每一个列表项高度rowHeight设置32px,显示数量viewSize设置5个,外层容器的高度为rowHeight * viewSize;
- 支撑区域的高度固定,总的列表项数目是count,那么支撑区域高度为rowHeight * count;
- 可视区域position设置absolute脱离文档流,然后计算偏移量,使用transform跟随滚动条移动位置;
- 其中监听onscroll事件的逻辑最为关键
单独截取出来
/**
* 滚动时更新显示区域的数据和高度
* @param {DOM.event} e
*/
const onScroll = e => {
let scrollTop = e.target.scrollTop;
let offset = scrollTop - (scrollTop % rowHeight);
let index = Math.floor(scrollTop / rowHeight);
setStartOffset(offset);
setStartIndex(index);
}
首先是获取当前滚动条的位置
let scrollTop = e.target.scrollTop;
滚动条位置变化时,计算渲染区域的偏移量
let offset = scrollTop - (scrollTop % rowHeight);
setStartOffset(offset);
计算展示的数据的索引
let index = Math.floor(scrollTop / rowHeight);
setStartIndex(index);
数组的slice方法不会改变原数组,所以渲染时直接用slice方法截取,size是渲染的DOM数量,size比可视区域的列表项viewSize大一点可以起到缓冲作用
data.slice(startIndex, startIndex + size)
封装好之后的使用方法如下
import React from 'react';
const test = () => {
const data = ['这是一个数组'];
const renderNode = (data, item, index) => {
return <div key={index} onClick={() => console.log(data)}> { item } </div>
}
return (
<VirtualList
data={ data } // 总数据
count={ data.length } // 列表项数量
size={ 8 } // 可视区渲染DOM的列表项数量
viewSize={ 5 } // 可视区能看到的列表数量
rowHeight={ 32 } // 每个列表项的行高度
renderNode={ renderNode } // 渲染的每个列表项
/>
)
}
export default test;
结合下拉框使用的效果...