1. 應用場景
需要用列表的形式展示大量的數(shù)據(jù),本文章只針對規(guī)則的、等高且固定高度的列表。如圖:

虛擬列表
前端渲染大量數(shù)據(jù)時會造成頁面卡頓,原因之一是渲染的DOM節(jié)點太多,而虛擬列表只渲染可見區(qū)域的DOM節(jié)點,極大的優(yōu)化了渲染性能。
2. 思路
初次加載時,只渲染初始的一部分數(shù)據(jù),頁面滾動時,動態(tài)計算需要展示的數(shù)據(jù)和滾動的位置。為此,DOM的設(shè)計需要三個區(qū)域----容器、列表展示區(qū)域、支撐滾動條區(qū)域
- 容器:包裹列表展示區(qū)和滾動條支撐區(qū);
- 列表展示區(qū)域:真實渲染的列表項區(qū)域,也就是可見的列表項部分;
-
支撐滾動條區(qū)域:用于支撐容器的高度,使容器出現(xiàn)滾動條。
樣式的命名可以個性化一點......
<div class="jisl-container">
<div class="phantom"></div>
<div class="view">
<!-- item-1 -->
<!-- item-2 -->
<!-- ...... -->
<!-- item-n -->
</div>
</div>
3. 代碼實現(xiàn)
目錄結(jié)構(gòu),新建一個文件夾,然后在文件夾中新建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, // 渲染的數(shù)據(jù)
count, // 列表的數(shù)量、長度
size, // 可視區(qū)渲染的列表項數(shù)量(真實DOM節(jié)點數(shù)量)
viewSize, // 可視區(qū)能看到的列表數(shù)量, 數(shù)值比size小, 即DOM比可見數(shù)量多, 具有緩沖作用
rowHeight, // 每一行列表項的高度
renderNode, // 渲染的列表項DOM節(jié)點
} = props;
const [startIndex, setStartIndex] = useState(0); // 起始索引
const [phantomHeight, setPhantomHeight] = useState(0); // 占位區(qū)的高度
const [startOffset, setStartOffset] = useState(0); // 渲染區(qū)域偏移量
// 計算支撐滾動條區(qū)域的高度
useEffect(() => {
setPhantomHeight(rowHeight * count);
}, [count, rowHeight])
/**
* 滾動時更新顯示區(qū)域的數(shù)據(jù)和高度
* @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);
}
// 支撐區(qū)域
.phantom {
width: 100%;
background: #fff;
}
// 可視區(qū)列表項
.view {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
background: #fff;
}
- 外層容器設(shè)置overflow,只展示可見區(qū)域,并且position設(shè)置relative。每一個列表項高度rowHeight設(shè)置32px,顯示數(shù)量viewSize設(shè)置5個,外層容器的高度為rowHeight * viewSize;
- 支撐區(qū)域的高度固定,總的列表項數(shù)目是count,那么支撐區(qū)域高度為rowHeight * count;
- 可視區(qū)域position設(shè)置absolute脫離文檔流,然后計算偏移量,使用transform跟隨滾動條移動位置;
- 其中監(jiān)聽onscroll事件的邏輯最為關(guān)鍵
單獨截取出來
/**
* 滾動時更新顯示區(qū)域的數(shù)據(jù)和高度
* @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;
滾動條位置變化時,計算渲染區(qū)域的偏移量
let offset = scrollTop - (scrollTop % rowHeight);
setStartOffset(offset);
計算展示的數(shù)據(jù)的索引
let index = Math.floor(scrollTop / rowHeight);
setStartIndex(index);
數(shù)組的slice方法不會改變原數(shù)組,所以渲染時直接用slice方法截取,size是渲染的DOM數(shù)量,size比可視區(qū)域的列表項viewSize大一點可以起到緩沖作用
data.slice(startIndex, startIndex + size)
封裝好之后的使用方法如下
import React from 'react';
const test = () => {
const data = ['這是一個數(shù)組'];
const renderNode = (data, item, index) => {
return <div key={index} onClick={() => console.log(data)}> { item } </div>
}
return (
<VirtualList
data={ data } // 總數(shù)據(jù)
count={ data.length } // 列表項數(shù)量
size={ 8 } // 可視區(qū)渲染DOM的列表項數(shù)量
viewSize={ 5 } // 可視區(qū)能看到的列表數(shù)量
rowHeight={ 32 } // 每個列表項的行高度
renderNode={ renderNode } // 渲染的每個列表項
/>
)
}
export default test;
結(jié)合下拉框使用的效果...

效果