虚拟列表
虚拟列表(Virtual List)是一种优化长列表性能的技术,它通过只渲染可视区域内的元素来大幅减少DOM节点数量,从而提高页面性能。基本原理如下
- 计算可视区域:确定容器高度和滚动位置
- 计算可见项:根据滚动位置确定哪些列表项应该显示
- 动态渲染:只渲染可见项,移除不可见项的DOM
- 占位元素:使用占位元素保持滚动条的正确性
DOM 结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <div {} className={'scroll-container'} ref={scrollContainerRef} onScroll={handleScroll} > {} <div className={'scroll-runway'} style={{ height: actualHeight }}> {} <div className={'scroll-placement'} style={{ transform: `translateY(${translateY}px)` }} > {} {renderList.map((item, index) => ( <ListItem key={item.id} {...item} onHeightChange={height => { if (height !== cachedHeightMap.get(item.id)) { cachedHeightMap.set(item.id, height); setRefreshTime(Date.now()) } }} /> ))} </div> </div> </div>
|
ResizeObserver
对于不定高的虚拟列表,由于我们不知道每一项的高度,所以我们给一个每一项的预估高度 estimateHeight
通过 ResizeObserver 来监听每一行的高度,后面将每一行的高度缓存起来。
ListItem 组件如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import React, { useEffect, useRef } from 'react'; import './index.css' const ListItem = (props) => { const itemRef = useRef(null) useEffect(() => { const observer = new ResizeObserver((entries) => { const { borderBoxSize: [{ blockSize }] } = entries[0] if (blockSize) { props.onHeightChange?.(blockSize) } }) observer.observe(itemRef.current) return () => { if (itemRef.current) { observer.unobserve(itemRef.current) } } }, []); return ( <div className={`list-item list-item-${props.id}`} ref={itemRef}> <h4> {props.id} / {props.title} </h4> <p> {props.content} </p> </div> ) } export default ListItem
|
滚动监听
滚动时监听 scrollTop 的变化实时计算当前需要渲染的元素索引
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const getItemHeight = (item) => { return cachedHeightMap.get(item.id) || estimatedHeight }
const handleScroll = (e) => { const { scrollTop } = e.target; getRenderData(originalList, scrollTop) }
const getRenderData = (list, scrollTop) => { let anchorIndex = 0; let height = 0; while (scrollTop >= height) { height += getItemHeight(originalList[anchorIndex]); anchorIndex++; } const startIndex = Math.max(0, anchorIndex - bufferSize) const endIndex = Math.min(startIndex + visibleCount.current + bufferSize, list.length - 1) setRenderList(list.slice(startIndex, endIndex)) setStartIndex(startIndex) }
|
偏移计算
通过 startIndex 计算出当前 scroll-placement translateY 的值
1 2 3 4 5 6 7 8 9 10 11
| const translateY = useMemo(() => { let y = 0 originalList.forEach((item, index) => { if (index >= startIndex) { return } y += getItemHeight(item) }) return y }, [startIndex])
|
预览
点击去预览 https://webengineerli.github.io/react-virtual-list
源码地址 https://github.com/WebEngineerLi/react-virtual-list