用React實(shí)現(xiàn)一個(gè)簡單的虛擬列表
虛擬列表是現(xiàn)在比較常用的前端渲染大數(shù)據(jù)列表的方案,目前也有很多組件庫和工具庫也都有對應(yīng)的實(shí)現(xiàn),如vueuse
和ahooks
的useVirtualList
、element-plus
的tableV2
等。對于初中級前端而言,虛擬列表也是面試的常客了,和面試官聊到性能優(yōu)化的話題時(shí)有時(shí)也會(huì)涉及到。本文將筆者對虛擬列表的一些認(rèn)知,以及基于認(rèn)知給出的實(shí)現(xiàn)做下整理。雖說現(xiàn)在網(wǎng)上寫虛擬列表的文章非常多質(zhì)量也非常高了,但對于技術(shù)人而言,只有自己進(jìn)行輸出,才能夠有更加深刻的理解。
虛擬列表的優(yōu)勢
比如我們現(xiàn)在要渲染一萬條數(shù)據(jù)的列表,如果直接使用循環(huán)進(jìn)行渲染,再加上列表項(xiàng)的dom
結(jié)構(gòu)比較復(fù)雜的話,渲染壓力是很大的。而虛擬列表只渲染出現(xiàn)在可視區(qū)域的數(shù)據(jù),這大大減少了瀏覽器的渲染壓力。
虛擬列表的原理
這邊給出的原理僅針對于最簡單的情形,即列表高度和列表項(xiàng)高度都是確定的。記列表高度為listHeight
,列表項(xiàng)高度為listItemHeight
。
虛擬滾動(dòng)的實(shí)現(xiàn)
在列表容器內(nèi)使用一個(gè)div
撐開滾動(dòng)條,這個(gè)div
的高度應(yīng)為listItemHeight * listItemNum
,其中listItemNum
為數(shù)據(jù)的總量。這一步是為了模擬渲染出全部數(shù)據(jù)情形下的滾動(dòng)條。監(jiān)聽滾動(dòng)條的事件,我們可以從列表容器的dom
對象中獲取到對應(yīng)的scrollTop
屬性,可以通過這個(gè)來計(jì)算渲染的起始元素索引以及渲染區(qū)域偏移量。
渲染區(qū)域的樣式
將列表容器設(shè)置為position:relative
,渲染數(shù)據(jù)區(qū)域容器設(shè)置為絕對定位。我們最終希望渲染數(shù)據(jù)區(qū)域出現(xiàn)到可視區(qū)域的正確位置,所以在我們操作滾動(dòng)條時(shí)要對渲染區(qū)域的偏移量進(jìn)行計(jì)算,在計(jì)算過后把渲染區(qū)域定位到正確的位置。定位的方式有兩種,一種基于top
,一種基于transform
。后面給出的實(shí)現(xiàn)將是基于第二種的。
計(jì)算渲染在可視區(qū)域的數(shù)據(jù)以及渲染區(qū)域的偏移量
列表可視區(qū)域中最大允許展示的列表項(xiàng)數(shù)量為Math.ceil(listHeight / listItemHeight)
,記為renderDataNum
。
當(dāng)我們操作滾動(dòng)條時(shí),實(shí)時(shí)獲取當(dāng)前列表容器的scrollTop
,可以計(jì)算得出渲染區(qū)域中的數(shù)據(jù)是從第幾個(gè)開始的,也就是渲染數(shù)據(jù)的起始位置索引,為Math.floor(scrollTop / itemHeight)
,記為renderDataStartIndex
。有了renderDataStartIndex
和renderDataNum
,我們就可以對原始數(shù)據(jù)進(jìn)行切片,獲取到渲染在可視區(qū)域的完整數(shù)據(jù)。
在這之后,我們需要調(diào)整這個(gè)區(qū)域相對于列表內(nèi)頂部的偏移量,實(shí)際上這個(gè)偏移量就是renderDataStartIndex * itemHeight
。(比如現(xiàn)在要渲染[0,1,2,3,4,5]
,可視區(qū)域只能渲染3個(gè)元素,當(dāng)我們滑動(dòng)使元素2
剛好處于列表頂部時(shí),此時(shí)的renderDataStartIndex
為2,而元素2的前面還有兩個(gè)元素會(huì)占用一定的高度,這個(gè)高度就是我們剛才計(jì)算的偏移量)
緩沖區(qū)的預(yù)留
當(dāng)我們實(shí)現(xiàn)完以上幾點(diǎn)后,會(huì)發(fā)現(xiàn)列表在快速滾動(dòng)時(shí)會(huì)出現(xiàn)閃動(dòng)的狀況,即滑動(dòng)過程中底下區(qū)域會(huì)變空白。這時(shí)候我們只需要在可視區(qū)域的上下預(yù)留相應(yīng)的緩沖區(qū)數(shù)據(jù)即可。
代碼實(shí)現(xiàn)
下面是一個(gè)比較簡單的用React
實(shí)現(xiàn)的例子,當(dāng)然可能有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); /** 真正用于存放虛擬列表項(xiàng)的容器 */ const listItemsContainerRef = useRef<HTMLDivElement>(null); /** 用于撐開滾動(dòng)條的容器高度(數(shù)據(jù)項(xiàng)個(gè)數(shù) * 每個(gè)數(shù)據(jù)項(xiàng)高度) */ const [scrollerContainerHeight, setScrollerContainerHeight] = useState<number>(0); /** 虛擬列表渲染的真實(shí)數(shù)據(jù) */ const [renderData, setRenderData] = useState<(typeof dataSource)[number][]>( [] ); /** 渲染數(shù)據(jù)區(qū)域偏移量 */ const [renderAreaOffset, setRenderAreaOffset] = useState<number>(0); /** 監(jiān)聽滾動(dòng)條變化,觸發(fā)計(jì)算渲染數(shù)據(jù)以及渲染區(qū)域偏移量 */ const handleScroll = useCallback(() => { /** 渲染元素個(gè)數(shù)(可視區(qū)域可容納最大元素?cái)?shù)量 + 緩沖區(qū)元素?cái)?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; }
對應(yīng)的使用例如下:
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: `標(biāo)題_${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)過去了快半年時(shí)間,或許是工作很忙,或許是進(jìn)入了舒適區(qū),學(xué)習(xí)方面的輸出相較于失業(yè)那段時(shí)間而言幾乎沒有。時(shí)間也過得很快,這一年也快過去了,未來會(huì)發(fā)生什么誰都不好說,所以還是要戒驕戒躁,繼續(xù)努力成長。
以上就是用React實(shí)現(xiàn)一個(gè)簡單的虛擬列表的詳細(xì)內(nèi)容,更多關(guān)于React實(shí)現(xiàn)虛擬列表的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react-redux及redux狀態(tài)管理工具使用詳解
Redux是為javascript應(yīng)用程序提供一個(gè)狀態(tài)管理工具集中的管理react中多個(gè)組件的狀態(tài)redux是專門作狀態(tài)管理的js庫(不是react插件庫可以用在其他js框架中例如vue,但是基本用在react中),這篇文章主要介紹了react-redux及redux狀態(tài)管理工具使用詳解,需要的朋友可以參考下2023-01-01React中實(shí)現(xiàn)編輯框自動(dòng)獲取焦點(diǎn)與失焦更新功能
在React應(yīng)用中,編輯框的焦點(diǎn)控制和數(shù)據(jù)回填是一個(gè)常見需求,本文將介紹如何使用useRef和useEffect鉤子,在組件中實(shí)現(xiàn)輸入框自動(dòng)獲取焦點(diǎn)及失焦后更新數(shù)據(jù)的功能,文中通過代碼示例給大家講解的非常詳細(xì),需要的朋友可以參考下2024-01-01在?React?Native?中使用?CSS?Modules的配置方法
有些前端工程師希望也能像開發(fā) web 應(yīng)用那樣,使用 CSS Modules 來開發(fā) React Native,本文將介紹如何在 React Native 中使用 CSS Modules,需要的朋友可以參考下2022-08-08React18?中的?Suspense?API使用實(shí)例詳解
這篇文章主要為大家介紹了React18?中的?Suspense?API使用實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10