https://mp.weixin.qq.com/s/tB3NWN2qdgpqQRtnB_q9Bg
问题思考
页面为什么会卡顿
?难道是数据量太大
,js 运算不过来了吗???
带着这样的问题,到页面尝试下,直接在页面中一次性插入十万条数据
<ul id="test"></ul>
// 记录任务开始时间
let now = Date.now();
// 定义十万条数据
const total = 100000;
// 获取容器
let ul = document.getElementById('test');
// 将数据插入容器 #test 中
for (let i = 0; i < total; i++) {
let li = document.createElement('li');
li.innerText = '测试数据'+ i
ul.appendChild(li);
}
console.log('JS运行时间:',Date.now() - now);
// JS运行时间:187
setTimeout(()=>{
console.log('总运行时间:',Date.now() - now);
// 总运行时间:2844
},0)
我们对十万条记录进行循环操作,JS 的运行时间为 187ms
,还是蛮快的,但是最终渲染完成后的总时间是 2844ms
。
在 JS 的 Event Loop
中,当 JS 引擎所管理的执行栈中的事件
以及所有微任务事件
全部执行完后,才会触发渲染线程
对页面进行渲染
如果还不了解 js Event Loop
的同学,建议自行查询补充下基础知识,这里就不做过多总结,通过以上两次console
能够得出结论:
对于大量数据渲染的时候,JS 运算并不是性能的瓶颈,性能的瓶颈主要在于渲染阶段
既然是渲染
的锅,那我们就可以从渲染方面去优化:可视区域渲染
-只渲染可见部分,不可见部分不渲染
虚拟滚动列表
虚拟滚动列表
就是采用的可视区渲染
方式优化,是一种根据滚动容器元素的可视区域
来渲染长列表数据中某一个部分数据的技术。
首先要明白两个概念:
可视区域
:滚动列表的容器,可以纵向滚动,其视觉可见的区域就称之为可视区域(简而言之就是你滚动容器的高度)列表区域
:滚动容器元素的内部内容区域。假设有 100 条数据,每个列表项的高度是 30,那么可滚动的区域的高度就是 100 * 30。可滚动区域当前的具体高度值一般可以通过(滚动容器)元素的 scrollHeight 属性获取。用户可以通过滚动来改变列表在可视区域的显示部分
代码实现
用数组保存列表所有元素的位置,只渲染
可视区域内的
列表元素,当可视区滚动时,根据滚动的offset
大小以及所有列表元素的位置,计算在可视区应该渲染哪些元素。
实现步骤:
- 计算当前可见区域起始数据的
startIndex
- 计算当前可见区域结束数据的
endIndex
- 计算当前可见区域的数据,并渲染到页面中
- 计算
startIndex
对应的数据在整个列表中的偏移位置startOffset
,并设置到列表上 - 计算
endIndex
对应的数据相对于可滚动区域最底部的偏移位置endOffset
,并设置到列表上
用代码简单实现:
vue 版本:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<style>
.list-view {
height: 400px;
overflow: auto;
position: relative;
border: 1px solid #aaa;
}
.list-view-phantom {
/* 使用不可见区域,撑起这个列表,让列表的滚动条出现 */
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.list-view-content {
left: 0;
right: 0;
top: 0;
position: absolute;
}
.list-view-item {
padding: 5px;
color: #666;
line-height: 30px;
box-sizing: border-box;
}
[v-cloak] {
display: none;
}
</style>
<body>
<div id="app" v-cloak>
<div class="list-view" ref="scrollBox" @scroll="handleScroll">
<div
class="list-view-phantom"
:style="{height: contentHeight}"
></div>
<div ref="content" class="list-view-content">
<div
class="list-view-item"
:style="{height: itemHeight + 'px'}"
v-for="item in visibleData"
>
{{ item }}
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
new Vue({
el: "#app",
computed: {
contentHeight() {
return this.data.length * this.itemHeight + "px";
},
},
mounted() {
this.updateVisibleData();
},
data() {
return {
data: new Array(100).fill(1),
itemHeight: 30, // 列表每行的行高
visibleData: [],
};
},
methods: {
updateVisibleData(scrollTop = 0) {
const visibleCount = Math.ceil(
this.$refs.scrollBox.clientHeight / this.itemHeight
);
const start = Math.floor(scrollTop / this.itemHeight);
const end = start + visibleCount;
this.visibleData = this.data.slice(start, end); // 计算可见区域数据
this.$refs.content.style.webkitTransform = `translate3d(0, ${
start * this.itemHeight
}px, 0)`;
},
handleScroll() {
const scrollTop = this.$refs.scrollBox.scrollTop;
this.updateVisibleData(scrollTop);
},
},
});
</script>
</body>
</html>
react 版本:
// index.tsx
import React from 'react';
import VirtualList from './VirtualList';
const listdata = new Array(100000).fill(0);
<VirtualList
width='100%'
height={300}
itemCount={listdata.length}
itemSize={50}
renderItem={(data: any) => {
const { index, style } = data;
return (
<div key={index} style={{
...style,
backgroundColor: index % 2 === 0 ? '#fff' : '#eee'
}}>
{index + 1}
</div>
)
}}
/>
// VirtualList.tsx
import React, { useRef, useState, useMemo } from 'react';
interface VirtualListProps {
width: string | number;
height: number;
itemCount: number;
itemSize: number;
renderItem: (...args: any) => JSX.Element;
}
const VirtualList: React.FC<VirtualListProps> = ({
width,
height,
itemCount,
itemSize,
renderItem,
}) => {
const scrollBox = useRef({} as HTMLDivElement);
const [start, setStart] = useState(0);
const end = useMemo(() => {
const endIndex = start + Math.floor(height / itemSize) + 1;
return endIndex > itemCount ? itemCount : endIndex;
}, [start]);
const visibleList = useMemo(() => {
return new Array(end - start).fill(0).map((item, index)=>({ item, index: start + index }));
}, [start, end]);
const styles = useMemo(
() => ({
position:'absolute',
top:0,
left:0,
width:'100%',
height: itemSize,
}),
[itemSize]
);
const handleScroll = () => {
const startIndex = Math.floor(scrollBox.current.scrollTop / itemSize);
setStart(startIndex);
};
return (
<div
ref={scrollBox}
style={{ overflow: 'auto', willChange: 'transform', width, height, border: '1px solid #000' }}
onScroll={handleScroll}
>
<div style={{ position: 'absolute',width:'100%',height: `${itemCount * itemSize}px` }}>
{
visibleList.map(({ index }) => renderItem({ index, style: { ...styles, top: itemSize * index } }))
}
</div>
</div>
)
};
export default VirtualList;
转载请注明:有爱前端 » 高性能渲染十万条数据不卡顿【虚拟滚动】