什么是虚拟列表?
就是针对大量数据,例如有10000条数据,只渲染可视区域内的数据,其他的数据不渲染。通过监听scroll事件,来改变页面内的数据。
虚拟列表是对长列表渲染的一种优化,解决大量数据渲染时,造成的渲染性能瓶颈的问题。
方法1:监听scrool事件
import { useEffect, useState } from 'react'
import './App.css'
const list = []
for (let i = 0; i < 10000; i++) {
list.push({
id: i,
content: '我是列表 ' + i
})
}
function App () {
const [start, setStart] = useState(0) // 列表起始位置
const [end, setEnd] = useState(20) // 列表中止位置
const buffer = 5 // 缓冲数据
const itemHeight = 50 // 每一项高度
const total = 10000 // 数据总数
const height = itemHeight * total // 列表总高度
const [virtualList, setVirtualList] = useState([])
// 初始化
useEffect(() => {
console.log(11);
setVirtualList([...list.slice(start, end)])
}, [])
function handleScroll (e) {
console.log('滚动啦', e);
const viewHeight = e.target.clientHeight // 可视高度
const scrollTop = e.target.scrollTop // 超出可视区域的高度
const newStart = scrollTop / itemHeight // 即将离开可视区的起始index
const newEnd = (viewHeight + scrollTop) / itemHeight // 即将进入可视区的起始index
const startIndex = Math.max(Math.floor(newStart - buffer), 0)
const endIndex = Math.min(Math.floor(newEnd + buffer), list.length - 1)
setStart(startIndex)
setEnd(endIndex)
setVirtualList([...list.slice(startIndex, endIndex)])
}
return (
<div className="virtual-list" onScroll={handleScroll}>
<div className="virtual-list-container" style={{ height: height + 'px' }}>
{virtualList.length && virtualList.map((item, index) => {
return <span style={{ height: itemHeight + 'px', top: (itemHeight * (index == 0 ? start : index == virtualList.length - 1 ? end - 1 : start + index)) + 'px' }} key={item.id}>{item.content}</span>
})}
</div>
</div>
)
}
export default App
html,body,#root,.virtual-list {
height: 100%;
overflow: auto;
padding: 0;
margin: 0;
}
.virtual-list-container {
display: flex;
flex-direction: column;
position: relative;
}
span {
border: 1px solid;
position: absolute;
left: 0;
right: 0;
}
span:nth-child(n) {
border-bottom: none;
}
span:last-child {
border-bottom: 1px solid;
}
方法二:通过监听元素是否进入可视区(IntersectionObserver ):
IntersectionObserver 可以观察元素是否进入可视区域内,通过判定起始元素和结尾元素是否正在进入可视区域,来改变需要展示的起始坐标。
start:开始下标,给起始元素绑定id用于监听
end:结束下标,给结尾元素绑定id用于监听
当鼠标向上滚动时,我们判定起始元素是否进入可视区域,进入则重新计算起始下标和结束下标。当鼠标向下滚动时,我们判定结尾元素是否进入可视区域,进入则重新计算起始下标和结束下标。
注意:要保证起始坐标和结束坐标都要在不可见的范围内,假设:设定可见范围内数据是20个,起始坐标和结束坐标相差25。
- 当鼠标向上滚动时,新的结束坐标在可视区域的下一个,此时start坐标在可视区最上方,也就是newEnd = start+20,newStart要保证在可视的20个数据之前并且保持25范围差,也就是newStart = start - 25 + 20。
- 当鼠标向下滚动时,新的起始坐标在可视数据的上一个,此时end坐标在可视区最下方,也就是newStart = end - 10,newEnd 要保证在可视的20个数据之后并且保持25范围差,也就是newEnd= end + 25 - 10。
- 即将滚动到最顶端时,newStart在(start - 25 + 20)和0之间取最大值
- 即将滚动到最低端时,newEnd在(end + 25 - 20)和maxEnd之间取最小值
实现代码如下:
import { useEffect, useRef, useState } from 'react'
import './App.css'
const list = []
for (let i = 0; i < 10000; i++) {
list.push({
id: i,
content: '我是列表 ' + i
})
}
function App () {
const [start, setStart] = useState(0) // 列表起始位置
const [end, setEnd] = useState(20) // 列表中止位置
const topEl = useRef()
const bottomEl = useRef()
const emptyRef = useRef()
const buffer = 5 // 缓冲数据
const itemHeight = 50 // 每一项高度
const total = 10000 // 数据总数
const height = itemHeight * total // 列表总高度
useEffect(() => {
const ob = new IntersectionObserver(callback)
// 分别观察开头和结尾的元素
if (topEl.current) {
ob.observe(topEl.current)
}
if (bottomEl.current) {
ob.observe(bottomEl.current)
}
return () => {
ob && ob.unobserve(topEl.current)
ob && ob.unobserve(bottomEl.current)
}
}, [end])
const callback = (entries) => {
entries.forEach((entry) => {
// entry.isIntersecting = true or entry.intersectionRatio > 0 进入可视区域
const maxEndIndex = list.length - 1
// 鼠标向上滚动
if (entry.isIntersecting && entry.target.id == 'top') {
const newStart = Math.max((start - buffer), 0)
const newEnd = Math.min((start + 20), maxEndIndex)
setStart(newStart)
setEnd(newEnd)
}
// 鼠标向下滚动
if (entry.isIntersecting && entry.target.id == 'bottom') {
const newStart = Math.max((end - 20), 0)
const newEnd = Math.min((end + buffer), maxEndIndex)
setStart(newStart)
setEnd(newEnd)
}
})
}
// 初始化
const virtualList = list.slice(start, end)
const lastIndex = virtualList.length - 1
return (
<div className="virtual-list">
<div className="virtual-list-container" style={{ height: height + 'px' }}>
{virtualList.length && virtualList.map((item, index) => {
const refVal = index == 0 ? topEl : index == lastIndex ? bottomEl : emptyRef
const id = index === 0 ? 'top' : (index === lastIndex ? 'bottom' : '')
const top = (itemHeight * (index == 0 ? start : index == virtualList.length - 1 ? end - 1 : start + index))
return <span ref={refVal} style={{ height: itemHeight + 'px', top: top + 'px' }} key={item.id} id={id} >{item.content}</span>
})}
</div>
</div>
)
}
export default App