场景
当需求是需要一次性渲染几千上万甚至十万或更多条数据的时候,如果我们一次性在页面上渲染会导致页面渲染很慢甚至卡死
优化方法
- 时间分片
- 虚拟列表
时间分片
使用 requestAnimationFrame + document.createDocumentFragment()
requestAnimationFrame 更符合浏览器的刷新节奏,配合 document.createDocumentFragment 批量更新 DOM,更节省渲染性能
<body>
<ul id="container"></ul>
<script>
const total = 100000;
const once = 20;
const page = total / once;
let index = 0;
const ul = document.getElementById('container');
function loop(curTotal, curIndex) {
const pageCount = Math.min(once, curTotal);
requestAnimationFrame(() => {
// 创建一个文档碎片,是一个虚拟的DOM结构
const fragment = document.createDocumentFragment();
for (let i = 0; i < pageCount; i++) {
const li = document.createElement('li');
li.innerText = `${curIndex + i}: ${Math.random()}`;
fragment.appendChild(li);
}
// 固定每20个li只回流一次
ul.appendChild(fragment);
if (curTotal > pageCount) {
loop(curTotal - pageCount, curIndex + pageCount);
}
});
}
loop(total, index);
</script>
</body>
虚拟列表
只渲染可见区域的数据,而非一次性渲染所有数据
- 初始化:设置一个固定高度的容器和数据源。
- 计算可见区域:通过容器高度和单个数据项高度,计算当前可见的数据条数。
- 监听滚动事件:实时更新需要渲染的起始和结束数据索引。
- 动态渲染:只更新当前视口内的 DOM 元素,减少渲染开销。
将虚拟列表封装成一个组件,在多个需要调用的地方直接调用即可
<template>
<div ref="listRef" class="infinite-list-container" @scroll="scrollEvent()">
<div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>
<div class="infinite-list" :style="{ transform: getTransform }">
<div
class="infinite-list-item"
v-for="item in visibleData"
:key="item.id"
:style="{ height: itemSize + 'px', lineHeight: itemSize + 'px' }"
>
{{ item.value }}
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, reactive, ref, computed } from 'vue';
const props = defineProps({
listData: [],
// 每个item的高度
itemSize: {
type: Number,
default: 50,
},
});
const state = reactive({
screenHeight: 0, // 可视区域的高度
startOffset: 0, // 偏移量
start: 0, // 起始数据下标
end: 0, // 结束数据下标
});
// 可视区域显示的数据条数
const visibleCount = computed(() => {
return state.screenHeight / props.itemSize;
});
// 可视区域显示的真实数据
const visibleData = computed(() => {
return props.listData.slice(state.start, Math.min(props.listData.length, state.end));
});
// 当前列表总高度
const listHeight = computed(() => {
return props.listData.length * props.itemSize;
});
// list跟着父容器移动了,现在列表要移回来
const getTransform = computed(() => {
return `translateY(${state.startOffset}px)`;
});
// 获取可视区域的高度
const listRef = ref(null);
onMounted(() => {
state.screenHeight = listRef.value.clientHeight;
state.end = state.start + visibleCount.value;
});
const scrollEvent = () => {
let scrollTop = listRef.value.scrollTop;
state.start = Math.floor(scrollTop / props.itemSize);
state.end = state.start + visibleCount.value;
state.startOffset = scrollTop - (scrollTop % props.itemSize);
};
</script>
<style lang="css" scoped>
.infinite-list-container {
height: 100%;
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch; /* 启用触摸滚动 */
}
.infinite-list-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1; /* 置于背景层 **/
}
.infinite-list {
position: absolute;
left: 0;
top: 0;
right: 0;
text-align: center;
}
.infinite-list-item {
border-bottom: 1px, solid, #000;
box-sizing: border-box;
}
</style>
页面使用
<template>
<div class="app">
<virtualList :listData="data" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import virtualList from './components/virtualList.vue';
// 数据源
const data = ref([]);
for (let i = 0; i < 1000; i++) {
data.value.push({ id: i, value: i });
}
</script>
<style lang="css" scoped>
.app {
width: 300px;
height: 400px;
border: 1px solid #000;
}
</style>