React虚拟列表|青训营笔记
这是我参与「第四届青训营 」笔记创作活动的的第10天
一、前言
在开发项目中,可能会遇到这样一个需求,需要加载一个长列表,数据有很多条,上万甚至上千条,这时候我们总不能 把所有的数据都挂载到dom上去把,这样页面的性能会降低到极限
二、思路
因此,可以使用一个虚拟列表来代替真实的列表,也就是说,只要挂载可视区域的数据就行了,其它不可见的地方就不挂载
三、具体实现
1. 高度准备
- 定义滚动区域的屏幕高度
- 定义所有列表的高度(总列表的高度,用于撑开容器经行滚动)
- 定义包裹内容的高度,用来跟随上下位移
2. 滚动值的计算
- 计算需要加载的列表数:
1+Math.ceil(屏幕高度/ 元素列表高度)多加载一项是为了防抖 - 开始值:
Math.floor(scrollTop / 元素列表高度)屏幕高度/单一元素高度 得到滚动了多少元素 - 结束值: 开始值+列表数
- transformY: 下拉的位移 根据滚动过多少元素,得到向下的位移 (开始值*每个元素的高度)
3. 实现思路
- onScroll 滚动事件 监听 scrollTop的值 从而改变start(开始位置)的值,使用useState()
- end 位置 通过监听 start、limit、list 的值是否改变,是用 useMemo()重置
- renderList 对列表数组经行截取 只渲染 start--end 部分
- renderList 里需要有传入过来的 jsx,用于渲染
4.代码部分
ReactVirtualList.jsx
import React, { useState, useMemo, memo } from 'react'
import './css.less'
import { useCallback } from 'react';
import { useRef } from 'react';
const ReactVirtualList = memo((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) => (
<span key={v.id} id={v.id} itemHeight={itemHeight} >
<Item value={v.v}></Item>
</span>
))
}, [start, end, list, itemHeight]);
const transformY = useMemo(() => {
return start * itemHeight + 'px'
}, [start, itemHeight]);
return (
<div className='island-virtual-list'
ref={listDom}
onScroll={(e) => scrollHandler(e)}
style={{ width: 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>
</div>
)
})
export default ReactVirtualList
css.less
.island-virtual-list {
position: absolute;
margin: auto;
top: 0;
left: 0;
bottom: 0;
right: 0;
opacity: 0.8;
overflow-y: auto;
overflow-x: hidden;
border: 1px solid #000;
}
.listWrapper {
display: flex;
flex-direction: column;
width: 100%;
}
.itemWrapper {
transform: translate3d(0, 0, 0);
}
App.jsx
import React, { memo } from 'react'
import ReactVirtualList from './ReactVirtualList';
// 定义可是滚动的样式
const styleObj = {
contentWidth: 800,
contentHeight: 300,
itemHeight: 10,
itemWidth: 60
}
// 被传入的jsx, 最终渲染的每一条的 样式
const Item = (props) => {
return <div className='list-item'>{props.value}</div>
}
// 数据列表
const list = Array(Math.floor((Math.random() + 1) * 10000)).fill().map((v, i, arr) => ({ id: i, v: i + '/' + arr.length + ' 行' }))
const App = memo(() => {
return <div className='app'>
<ReactVirtualList {...styleObj} list={list} item={Item} ></ReactVirtualList>
</div>
})
export default App
效果
5. 在具体项目中使用
index.jsx
import React, { memo } from 'react'
import ReactVirtualList from '../../../../../components/ReactVirtualList'
import calculateTimeLength from '../../../../../utils/calculateTimeLength'
import { MioSonglistBottomSonglistDiv } from './css'
// 定义可是滚动的样式
const styleObj = {
contentHeight: 500,
itemHeight: 35,
}
// 渲染的一条数据
const songItem =(props) => {
return (
<div className='song-list-item'>
<span className="act">
<span className="number">{props.value.index<10?`0${props.value.index}`:props.value.index}</span>
<span className='icon'>❤</span>
</span>
<span className="title">{props.value.item.name}</span>
<span className="singer">
{
props.value.item.ar.map((item,index) => {
return (
<span key={item.id}>
<span>{item.name}</span>
{index+1 !=props.value.item.ar.length && <span>/</span>}
</span>
)
})
}
</span>
<span className="album">{props.value.item.al.name}</span>
<span className="time">{calculateTimeLength(props.value.item.dt)}</span>
</div>
)
}
// 操作 标题 歌手 专辑 时间
const MioSonglistBottomSonglist = memo((props) => {
const {songlist} = props;
console.log(songlist[0]);
return (
<MioSonglistBottomSonglistDiv onScroll={e=>{console.log(e.target.scrollTop)}}>
<div className="song-list-item-top">
<span className="act top">操作</span>
<span className="title top">歌曲</span>
<span className="singer top">歌手</span>
<span className="album top">专辑</span>
<span className="time top">时间</span>
</div>
<ReactVirtualList {...styleObj} list={songlist} item={songItem}/>
{
songlist.length<=10 && <div>loading</div>
}
</MioSonglistBottomSonglistDiv>
)
})
export default MioSonglistBottomSonglist
tips
因为是放在组件中,因此宽度是依赖父组件自适应的,所以使用时不用再设置宽度。
效果如下
效果还是很不错的