React 长列表加载 实现虚拟列表
效果
实现思路
定义一个container 高为一屏高
定义一个listWrapper 高为所有列表元素的高度,来撑开容器
定义一个itemWrapper 高为一屏高度,来跟随上、下拉操作进行位移,从而总是覆盖展示在当前屏
滚动时关键值计算:
一屏个数 limit: 1+(Math.ceil(containerHeight / itemHeight)) 加1是预加载,多加载一项减少抖动
start: 一屏的起始索引 Math.floor(scrollTop / itemHeight) 即滚动了几个元素
end: 一屏的结束索引 start+limit
transformY:
下拉的位移。 滚动时,根据scrollTop计算出滚动过多少个元素,设置对应的start和end, 那么listWrapper的transformY值即为start*itemHeight(一屏起始索引*列表项高度)。下拉多少,就向下位移多少,让其一直展示在首屏
具体实现
ReactVirtualList
import React, { useState, useMemo, memo } from 'react'
import './ReactVirtualList.css'
import { ListItem } from './ListItem';
import { useCallback } from 'react';
import { useRef } from 'react';
const ReactVirtualList = (props) => {
let { list, item: Item, contentWidth, contentHeight, itemHeight } = props
const [start, setStart] = useState(0)
const listDom = useRef()
const limit = useMemo(() => {
return 1 + Math.ceil(contentHeight / (itemHeight))
}, [contentHeight, itemHeight]);
const scrollHandler = useCallback(
(e) => {
const top = e.target.scrollTop
const curStart = Math.floor(top / (itemHeight))
curStart !== start && setStart(curStart)
},
[itemHeight, start]
)
const end = useMemo(() => {
return Math.min(start + limit, list.length)
}, [start, limit, list]);
const renderList = useMemo(() => {
return list
.slice(start, end)
.map((v, i) => (
<ListItem key={v.id} id={v.id} itemHeight={itemHeight} >
<Item text={v.v}></Item>
</ListItem>
))
}, [start, end, list, itemHeight]);
const transformY = useMemo(() => {
return start * itemHeight + 'px'
}, [start, itemHeight]);
return <ul className='island-virtual-list' ref={listDom} onScroll={(e) => scrollHandler(e)} style={{ contentWidth + 'px', height: contentHeight + 'px' }}>
<div className="listWrapper" style={{ height: itemHeight * list.length + 'px' }}>
<div className="itemWrapper" style={{ height: contentHeight + 'px', transform: `translate3d(0, ${transformY}, 0)` }}>
{renderList}
</div>
</div>
</ul>
}
export default memo(ReactVirtualList)
样式
.island-virtual-list {
margin: auto;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
opacity: 0.8;
overflow-y: auto;
overflow-x: hidden;
border: 1px solid #ccc;
}
.listWrapper {
display: flex;
flex-direction: column;
100%;
}
.island-virtual-list-item {
box-sizing: border-box;
100%;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid black;
}
.itemWrapper {
transform: translate3d(0, 0, 0);
}
使用
import './App.css';
import ReactVirtualList from './component/ReactVirtualList';
const styleObj = {
contentWidth: 800,
contentHeight: 300,
itemHeight: 50,
itemWidth: 60
}
const Item = (props) => {
return <div className='list-item'>{props.text}</div>
}
const list = Array(Math.floor((Math.random() + 1) * 10000)).fill().map((v, i, arr) => ({ id: i, v: i + '/' + arr.length + ' 行' }))
function App() {
return <div className='app'>
<ReactVirtualList {...styleObj} list={list} item={Item} ></ReactVirtualList>
</div>
}
export default App;