浅聊一下
后端一次性给我传了十万条数据,我真想提刀去和他对线,奈何打不过,擦干眼泪,硬着头皮上...别误会,是硬着头皮写代码...
模拟十万条数据
html
<!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>
<body>
<div id="container"></div>
<script>
const total = 100000
let ul = document.createElement('ul');
let div = document.getElementById('container');
div.appendChild(ul);
for(let i = 0;i<total;i++){
let li = document.createElement('li');
li.innerText = ~~(Math.random()*total);
ul.appendChild(li);
}
</script>
</body>
</html>
当我在浏览器上打开他的时候,电脑风扇已经顶不住了,开始嗡嗡作响,那我们该如何来渲染这段数据呢?
setTimeout
第一个思路,既然我们一次性加载不了这么多图片,那么我们能不能分很多次加载呢?说干就干
html
<!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>
<body>
<ul id="container"></ul>
<script>
const total = 10000;
let ul = document.getElementById('container'); // 修正:获取 #container 元素
let once = 20;
let page = Math.ceil(total / once); // 修正:计算页数
let index = 0;
function loop(curTotal) {
if (curTotal <= 0) {
return;
}
let pageCount = Math.min(curTotal, once);
setTimeout(() => {
for (let i = 0; i < pageCount; i++) {
let li = document.createElement('li'); // 修正:使用正确的标签名称 'li'
li.innerText = ~~(Math.random() * total);
ul.appendChild(li);
}
loop(curTotal - pageCount);
}, 0); // 修正:指定延迟时间
}
loop(total);
</script>
</body>
</html>
- 在这里,我们一次只加载20条数据,page就是我们要加载的次数
- 如果剩余数据少于20条,那么用pageCount = Math.min(curTotal, once);来计算
- 递归遍历,直到所有的数据全部渲染完成
这样,我们的十万条数据就成功渲染在了页面上,但是我发现了一个问题,当我向下滚动页面的时候,总是会存在短暂的白屏,不停地查找之下我发现:由于dom结构的渲染和页面的刷新率不同步造成闪烁
requestAnimationFrame + fragment(时间分片)
html
<!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>
<body>
<ul id="container"></ul>
<script>
let now = Date.now();
const total = 10000;
let ul = document.getElementById('container'); // 修正:获取 #container 元素
let once = 20;
let page = Math.ceil(total / once); // 修正:计算页数
let index = 0;
function loop(curTotal) {
if (curTotal <= 0) {
return;
}
let pageCount = Math.min(curTotal, once);
window.requestAnimationFrame(() => {
let fragment
for (let i = 0; i < pageCount; i++) {
fragment = document.createDocumentFragment();//创建一个虚拟的文档片段
let li = document.createElement('li'); // 修正:使用正确的标签名称 'li'
li.innerText = ~~(Math.random() * total);
fragment.appendChild(li);
}
ul.appendChild(fragment);
loop(curTotal - pageCount);
})
}
loop(total);
</script>
</body>
</html>
在我们使用setTimeout方法来渲染数据的时候,因为每渲染一条数据都要操作DOM,所以性能不好,并且会造成闪烁,这里我们使用文档碎片和window.requestAnimationFrame()来解决这个问题...
- 文档碎片
文档片段(DocumentFragment)是DOM中的一种节点类型,它允许将一组子节点添加到文档树中,而无需像普通操作一样频繁地更新DOM。文档片段可以看作是一个轻量级的虚拟容器,用于临时存储节点,然后一次性地将它们添加到文档中,这样可以减少DOM操作,提高性能。
- window.requestAnimationFrame()
window.requestAnimationFrame()
是一个用于在浏览器重绘之前执行指定的函数的方法。它通常用于执行动画和其他需要在每一帧之间更新的任务,以确保动画的流畅性并提高性能。
主要特点包括:
- 与屏幕刷新同步 :
requestAnimationFrame()
方法会在浏览器准备好重绘页面时执行指定的函数,通常是在下一帧绘制之前。这样可以确保动画和其他操作与屏幕刷新同步,避免了丢帧和动画不连贯的问题。 - 自动调节帧率:浏览器会根据当前设备的性能和显示器的刷新率来动态调节帧率,以确保最佳的用户体验。这意味着在性能较差的设备上,帧率会自动降低,而在性能较好的设备上,帧率会自动提高。
- 优化性能 :由于
requestAnimationFrame()
方法会在浏览器重绘之前执行,因此可以更好地利用浏览器的重绘优化机制,从而提高性能并减少功耗。
我们将每一页的数据先放在文档碎片里,再将文档碎片作为子节点挂载到ul上
虚拟列表
掘友们想想,我们看不见的数据有必要渲染吗?联想图片懒加载,看不见的数据我直接不加载就完了...
我们只需要渲染可视区域的内容就好了,缓冲区域是为了让我们的滚动更丝滑,不会出现白屏闪烁的现象
- 拿到可视区域
js
const boxHeight = ref(0)//可视区域的高度
const itemHeight = ref(40)//列表项的高度
const scrollBox = ref(null)//可视区域容器
onMounted(() => {
boxHeight.value = scrollBox.value.clientHeight
})
const itemNum = computed(() => {
return ~~(boxHeight.value / itemHeight.value) + 2
})
const startIndex = ref(0)//可视区域第一项的下标
- 滚动事件
js
const doScroll = () => {
const index = ~~(scrollBox.value.scrollTop / itemHeight.value)
if (index === startIndex.value) return
startIndex.value = index
}
const endIndex = computed(() => {//可视区域最后一项的索引
let index = startIndex.value + itemNum.value * 2
if (!allList.value[index]) {
index = allList.value.length - 1
}
return index
})
doScroll
函数:该函数是用来处理滚动事件的。它首先通过scrollBox.value.scrollTop
获取滚动容器的滚动距离,并根据列表项的高度itemHeight.value
计算出当前滚动到的列表项的索引。然后与startIndex
变量进行比较,如果计算出的索引与startIndex
相同,则表示可见区域内的列表项没有变化,直接返回,否则更新startIndex
变量为新的索引值。endIndex
计算属性:该计算属性用于计算可视区域内最后一个可见列表项的索引。它首先根据startIndex
和itemNum
计算出当前可见区域内的最后一个列表项的索引,并添加了一些额外的列表项数量,以确保在滚动过程中有足够的列表项可以填充整个可见区域。如果计算出的索引超出了列表的范围,则将其调整为列表的最后一个索引。
- 拿到渲染数据
js
const curList = computed(() => {
let index = 0
if (startIndex.value <= itemNum.value) {
index = 0
}else{
index = startIndex.value - itemNum.value
}
return allList.value.slice(index, endIndex.value+1)
})
- 首先,通过
computed
属性创建了curList
计算属性。 - 在计算
curList
的过程中,首先判断startIndex
是否小于等于itemNum
,如果是,则将index
设置为 0,表示从列表的第一项开始渲染。 - 如果
startIndex
大于itemNum
,则将index
设置为startIndex - itemNum
,这样可以确保在滚动时,始终在当前可见区域的前面预留一些列表项,以提供更流畅的滚动体验。 - 然后,通过
allList.value.slice(index, endIndex.value+1)
对allList
数组进行切片操作,获取从index
到endIndex
(包括endIndex
)范围内的列表项,作为当前需要渲染的列表项数组。
全代码:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
.v-scroll {
width: 300px;
height: 400px;
border: 1px solid #000;
margin: 100px 0 0 100px;
overflow-y: scroll;
}
li {
list-style: none;
padding-left: 20px;
height: 40px;
line-height: 40px;
border-bottom: 1px solid #d5cece;
box-sizing: border-box;
}
</style>
</head>
<body>
<div id="app">
<div class="v-scroll" ref="scrollBox" @scroll="doScroll">
<ul>
<li v-for="(item,index) in curList" :key="index">{{index+1}} ---- {{item}}</li>
</ul>
</div>
</div>
<script>
const { createApp, ref, onMounted, computed } = Vue
createApp({
setup() {
const allList = ref([])//所有的请求
const getAllList = (count) => {//接口请求
for (let i = 0; i < count; i++) {
allList.value.push(`我是列表${i + 1}项`)
}
}
getAllList(300)
//----------------------------------------------------------------------
const boxHeight = ref(0)//可视区域的高度
const itemHeight = ref(40)//列表项的高度
const scrollBox = ref(null)//可视区域容器
onMounted(() => {
boxHeight.value = scrollBox.value.clientHeight
})
const itemNum = computed(() => {
return ~~(boxHeight.value / itemHeight.value) + 2
})
const startIndex = ref(0)//可视区域第一项的下标
// 页面滚动
const doScroll = () => {
const index = ~~(scrollBox.value.scrollTop / itemHeight.value)
if (index === startIndex.value) return
startIndex.value = index
}
const endIndex = computed(() => {//可视区域最后一项的索引
let index = startIndex.value + itemNum.value * 2
if (!allList.value[index]) {
index = allList.value.length - 1
}
return index
})
// 拿到真正要被渲染的数据
const curList = computed(() => {
let index = 0
if (startIndex.value <= itemNum.value) {
index = 0
}else{
index = startIndex.value - itemNum.value
}
return allList.value.slice(index, endIndex.value+1)
})
return {
curList,
allList,
boxHeight,
itemHeight,
scrollBox,
itemNum,
startIndex,
doScroll,
endIndex
}
}
}).mount('#app')
</script>
</body>
</html>
结尾
擦干眼泪,后端还是咱的好伙计...