用React實現(xiàn)一個簡單的虛擬列表
虛擬列表是現(xiàn)在比較常用的前端渲染大數(shù)據(jù)列表的方案,目前也有很多組件庫和工具庫也都有對應的實現(xiàn),如vueuse和ahooks的useVirtualList、element-plus的tableV2等。對于初中級前端而言,虛擬列表也是面試的常客了,和面試官聊到性能優(yōu)化的話題時有時也會涉及到。本文將筆者對虛擬列表的一些認知,以及基于認知給出的實現(xiàn)做下整理。雖說現(xiàn)在網(wǎng)上寫虛擬列表的文章非常多質量也非常高了,但對于技術人而言,只有自己進行輸出,才能夠有更加深刻的理解。
虛擬列表的優(yōu)勢
比如我們現(xiàn)在要渲染一萬條數(shù)據(jù)的列表,如果直接使用循環(huán)進行渲染,再加上列表項的dom結構比較復雜的話,渲染壓力是很大的。而虛擬列表只渲染出現(xiàn)在可視區(qū)域的數(shù)據(jù),這大大減少了瀏覽器的渲染壓力。
虛擬列表的原理
這邊給出的原理僅針對于最簡單的情形,即列表高度和列表項高度都是確定的。記列表高度為listHeight,列表項高度為listItemHeight。
虛擬滾動的實現(xiàn)
在列表容器內(nèi)使用一個div撐開滾動條,這個div的高度應為listItemHeight * listItemNum,其中listItemNum為數(shù)據(jù)的總量。這一步是為了模擬渲染出全部數(shù)據(jù)情形下的滾動條。監(jiān)聽滾動條的事件,我們可以從列表容器的dom對象中獲取到對應的scrollTop屬性,可以通過這個來計算渲染的起始元素索引以及渲染區(qū)域偏移量。
渲染區(qū)域的樣式
將列表容器設置為position:relative,渲染數(shù)據(jù)區(qū)域容器設置為絕對定位。我們最終希望渲染數(shù)據(jù)區(qū)域出現(xiàn)到可視區(qū)域的正確位置,所以在我們操作滾動條時要對渲染區(qū)域的偏移量進行計算,在計算過后把渲染區(qū)域定位到正確的位置。定位的方式有兩種,一種基于top,一種基于transform。后面給出的實現(xiàn)將是基于第二種的。
計算渲染在可視區(qū)域的數(shù)據(jù)以及渲染區(qū)域的偏移量
列表可視區(qū)域中最大允許展示的列表項數(shù)量為Math.ceil(listHeight / listItemHeight),記為renderDataNum。
當我們操作滾動條時,實時獲取當前列表容器的scrollTop,可以計算得出渲染區(qū)域中的數(shù)據(jù)是從第幾個開始的,也就是渲染數(shù)據(jù)的起始位置索引,為Math.floor(scrollTop / itemHeight),記為renderDataStartIndex。有了renderDataStartIndex和renderDataNum,我們就可以對原始數(shù)據(jù)進行切片,獲取到渲染在可視區(qū)域的完整數(shù)據(jù)。
在這之后,我們需要調整這個區(qū)域相對于列表內(nèi)頂部的偏移量,實際上這個偏移量就是renderDataStartIndex * itemHeight。(比如現(xiàn)在要渲染[0,1,2,3,4,5],可視區(qū)域只能渲染3個元素,當我們滑動使元素2剛好處于列表頂部時,此時的renderDataStartIndex為2,而元素2的前面還有兩個元素會占用一定的高度,這個高度就是我們剛才計算的偏移量)
緩沖區(qū)的預留
當我們實現(xiàn)完以上幾點后,會發(fā)現(xiàn)列表在快速滾動時會出現(xiàn)閃動的狀況,即滑動過程中底下區(qū)域會變空白。這時候我們只需要在可視區(qū)域的上下預留相應的緩沖區(qū)數(shù)據(jù)即可。
代碼實現(xiàn)
下面是一個比較簡單的用React實現(xiàn)的例子,當然可能有bug,不過我這邊自測是沒啥問題的:
VirtualList/index.tsx:
import React, {
CSSProperties,
useEffect,
useRef,
useState,
useCallback,
} from "react";
import "./index.css";
interface IProps<T extends { key: React.Key }> {
height?: CSSProperties["height"];
width?: CSSProperties["width"];
dataSource: T[];
itemHeight?: number;
cacheNumber?: number;
listItemRender?: (item: T) => React.ReactNode;
}
export const VirtualList: <T extends { key: React.Key }>(
props: IProps<T>
) => React.ReactNode = (props) => {
const {
height = "500px",
dataSource = [],
width = "400px",
itemHeight = 32,
cacheNumber = 10,
listItemRender,
} = props;
/** 虛擬列表外層容器 */
const containerRef = useRef<HTMLDivElement>(null);
/** 真正用于存放虛擬列表項的容器 */
const listItemsContainerRef = useRef<HTMLDivElement>(null);
/** 用于撐開滾動條的容器高度(數(shù)據(jù)項個數(shù) * 每個數(shù)據(jù)項高度) */
const [scrollerContainerHeight, setScrollerContainerHeight] =
useState<number>(0);
/** 虛擬列表渲染的真實數(shù)據(jù) */
const [renderData, setRenderData] = useState<(typeof dataSource)[number][]>(
[]
);
/** 渲染數(shù)據(jù)區(qū)域偏移量 */
const [renderAreaOffset, setRenderAreaOffset] = useState<number>(0);
/** 監(jiān)聽滾動條變化,觸發(fā)計算渲染數(shù)據(jù)以及渲染區(qū)域偏移量 */
const handleScroll = useCallback(() => {
/** 渲染元素個數(shù)(可視區(qū)域可容納最大元素數(shù)量 + 緩沖區(qū)元素數(shù)量) */
const renderDataNum =
Math.ceil((containerRef.current?.clientHeight || 0) / itemHeight) +
cacheNumber;
/** 渲染元素起始索引 */
let renderDataStartIndex =
Math.floor((containerRef.current?.scrollTop || 0) / itemHeight) -
cacheNumber;
if (renderDataStartIndex < 0) {
renderDataStartIndex = 0;
}
setRenderData(
dataSource.slice(
renderDataStartIndex,
renderDataStartIndex + renderDataNum
)
);
setRenderAreaOffset(renderDataStartIndex * itemHeight);
}, [cacheNumber, dataSource, itemHeight]);
useEffect(() => {
setScrollerContainerHeight(dataSource.length * itemHeight);
handleScroll();
}, [dataSource, handleScroll, itemHeight]);
return (
<div
className="virtual-list-container"
ref={containerRef}
style={{ height, width }}
onScroll={handleScroll}
>
<div style={{ height: `${scrollerContainerHeight}px` }}></div>
<div
className="virtual-list-items-container"
ref={listItemsContainerRef}
style={{
width: "100%",
transform: `translate(0,${renderAreaOffset}px)`,
}}
>
{renderData.map((item) => (
<div style={{ height: `${itemHeight}px` }} key={item.key}>
{listItemRender
? listItemRender(item)
: typeof item === "object"
? JSON.stringify(item)
: (item as string)}
</div>
))}
</div>
</div>
);
};
VirtualList/index.css
.virtual-list-container {
overflow: auto;
position: relative;
}
.virtual-list-items-container {
position: absolute;
top: 0;
overflow: hidden;
}
對應的使用例如下:
import './App.css'
import { VirtualList } from './components/VirtualList'
interface ListItemData {
key: React.Key,
id: string | number
title: string
imgSrc: string
desc: string
}
const ListItem = (item: ListItemData) => {
return (
<div style={{ padding: '5px', width: '100%', boxSizing: 'border-box' }}>
<div
style={{ border: '1px solid #f0f0f0', boxShadow: '0 1px 2px -2px rgba(0,0,0,.16), 0 3px 6px 0 rgba(0,0,0,.12), 0 5px 12px 4px rgba(0,0,0,.09)', display: 'flex', alignItems: 'center', columnGap: 10 }}>
<img src={item.imgSrc} style={{ height: 100 }} />
<div style={{ display: 'flex', flexDirection: 'column',rowGap: 10 }}>
<div style={{ fontWeight: 'bold' }}>{item.title}</div>
<div>{item.desc}</div>
</div>
</div>
</div>
)
}
function App() {
return (
<div>
<VirtualList<ListItemData> listItemRender={ListItem} itemHeight={120} dataSource={new Array(200).fill(0).map((_, index) => ({
key: index,
id: index,
title: `標題_${index}`,
imgSrc: 'https://img14.360buyimg.com/n1/jfs/t1/83129/30/18124/355324/626b8c95Ea76bb2d9/b7c73d677df0c57d.jpg',
desc: 'fumofumo'
}))} />
</div>
)
}
export default App
效果圖:

寫在最后
距離找到工作到現(xiàn)在已經(jīng)過去了快半年時間,或許是工作很忙,或許是進入了舒適區(qū),學習方面的輸出相較于失業(yè)那段時間而言幾乎沒有。時間也過得很快,這一年也快過去了,未來會發(fā)生什么誰都不好說,所以還是要戒驕戒躁,繼續(xù)努力成長。
以上就是用React實現(xiàn)一個簡單的虛擬列表的詳細內(nèi)容,更多關于React實現(xiàn)虛擬列表的資料請關注腳本之家其它相關文章!
相關文章
React中hook函數(shù)與useState及useEffect的使用
這篇文章主要介紹了React中hook函數(shù)與useState及useEffect的使用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習吧2022-10-10
解決React報錯Rendered more hooks than during
這篇文章主要為大家介紹了React報錯Rendered more hooks than during the previous render解決方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12
React 無狀態(tài)組件(Stateless Component) 與高階組件
這篇文章主要介紹了React 無狀態(tài)組件(Stateless Component) 與高階組件,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08
Native?Memory?Tracking追蹤區(qū)域示例分析
這篇文章主要為大家介紹了Native?Memory?Tracking追蹤區(qū)域示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11

