通常在瀑布流中每次拉取下一页数据需要渲染大量数据的时候是非常耗时,会出现卡顿的现象。随着拉取的数据越来越多,列表渲染时间长、卡顿的问题越来越严重。
这个时候虚拟列表就派上用场了。虚拟列表的实现原理简单来说,就是列表并不会把所有的数据都渲染出来,而是通过监听滚动事件然后实时计算当前是哪几条数据显示在页面上,然后只渲染用户可以看见的这几条数据
效果截图:
可以看到,我生成了60个元素,但初始化渲染的时候容器只渲染了可看见的20个元素。那么我们在滚动一下滚动条看看效果。
滚动条处于在容器的中间的位置,我再次重新获取了父容器 children元素后,结果还是之渲染20个元素。虚拟列表效果是不是就实现了。
话不多说,直接给展示全部代码,里面有备注代码讲解!。可以直接复制粘贴到html文件中,方便你们预览效果。
以下是全部代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- 引入React -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<!-- 引入React DOM -->
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<!-- 引入Babel,用于JSX转换 -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<style>
.list-container {
overflow: auto;
box-sizing: border-box;
border: 1px solid black;
height: 800px;
width: 1300px;
margin: 30px;
}
.list-container-inner {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.list-container-inner-item {
width: 243px;
height: 370px;
outline: 1px solid black;
}
.inner-img {
height: 86%;
display: flex;
justify-content: center;
align-items: center;
}
.inner-img-text {
box-shadow: 0px 0px 6px 1px #fff;
padding: 8px;
}
</style>
</head>
<body>
<div id="root"></div>
</body>
<script type="text/babel">
function generateRandomColor() {//生成随机颜色
var letters = '0123456789ABCDEF';
var color = '#';
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
function createData() {//生成数据
const data = []
for (let i = 0; i < 60; i++) {
data.push({ id: i, content: `内容:${i}`, color: generateRandomColor() })
}
return data
}
const App = () => {
let [data, setData] = React.useState(createData)
let [virtualData, setVirtualData] = React.useState([])
let [_paddings, setPaddings] = React.useState([0, 0])
React.useEffect(() => {
let itemHeight = 370
let itemrWidth = 243
let outerWidth = 1300
let outerHeight = 800
const outerContainer = document.querySelector('.list-container')
// 把获取的数据,按每行能放置多少个元素进行坐标分组
let twoDimensionalCoordinates = []
function getCoordinate() {
let start = 0
let init_end = Math.floor(((outerWidth - (((outerWidth / itemrWidth) - 1) * 16)) / itemrWidth))// 计算外部容器一行能放多少个元素
let end = init_end
while (true) {
if (data.slice(start, end).length > 0) {
twoDimensionalCoordinates.push([start, end])
} else {
break;
}
start = end
end += init_end
}
}
getCoordinate()
const scrollCallback = () => {
// 获取当前要渲染的元素的坐标
const scrollTop = Math.max(outerContainer.scrollTop, 0)
const startIndex = Math.floor(scrollTop / itemHeight)
const endIndex = startIndex + Math.ceil(outerHeight / itemHeight)
// 从twoDimensionalCoordinates取出要渲染的元素并渲染到容器中
const viewData = twoDimensionalCoordinates.slice(startIndex, endIndex + 1)
const escapeData = viewData.reduce((pre, coordinate) => pre.concat(data.slice(coordinate[0], coordinate[1])), [])
setVirtualData(escapeData)
// 未渲染的元素由padding-top和padding-bottom代替,保证滚动条位置正确
const paddingTop = startIndex * itemHeight
const paddingBottom = (twoDimensionalCoordinates.length - endIndex) * itemHeight
setPaddings([paddingTop, paddingBottom])
}
// 首屏渲染
scrollCallback()
// 监听外部容器的滚动事件
outerContainer.addEventListener('scroll', scrollCallback)
return () => {
outerContainer.removeEventListener('scroll', scrollCallback)
}
}, [])
return <div className="list-container">
<div className='list-container-inner' style={{ paddingTop: _paddings[0], paddingBottom: _paddings[1] }}>
{virtualData.map((item) => {
return <div className='list-container-inner-item' key={item.id}>
<div className='inner-img' style={{ background: item.color }}>
<span className="inner-img-text">img容器</span>
</div>
<div>
<div>标题-{item.content}</div>
<div>{item.content}</div>
</div>
</div>
})}
</div>
</div>
}
// 挂载React应用
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</html>