背景
最近在公司的项目(vue)中要求展示一页显示500条数据,本以为简单的渲染出来就行了。没想到初次渲染的时间过长,在批量操作的时候更新列表也会导致页面卡顿。十分影响用户体验,本文记录了这次列表的优化过程。影响体验的主要原因是同事渲染的DOM过多,因此从这个方面入手。
原理
准备采用虚拟列表,虚拟列表
其实是按需显示的一种实现,即只对屏幕可视区域
进行渲染,对不可见区域
中的数据不渲染或部分渲染的方案,从而使长列表的能高性能渲染。
假设:
一共有500条
数据,单条数据高度为10px
,整个列表的高度就为5000px
,屏幕可视区域的高度为100px
初次加载
渲染最前面的10条
数据,其他数据隐藏
发生滚动
滚动了100px
之后,前10条
滚动到不可见区域,11-20条
在可见区域需要显示
预渲染
为了防止滚动后出现白屏,影响体验,所以要进行预渲染
,预留几条渲染
实现
html部分
InfiniteScrollContainer
是滚动的容器InfiniteScrollHeight
撑开容器 用于形成滚动条InfiniteScroll
展示渲染的列表InfiniteScrollItem
滚动列表的的单行渲染listHeight
为所有的单行高度相加translateY
是滚动条的偏移量
<div @scroll="scrollTops" class="InfiniteScrollContainer">
<div class="InfiniteScrollHeight" :style="{ height: listHeight + 'px' }"></div>
<div class="InfiniteScroll" :style="{ transform: 'translate3d(0,' + (renderList[0] ? renderList[0].infiniteScrollTop : 0) + 'px,0)' }">
<div class="InfiniteScrollItem" :data-key="item.infiniteScrollId" :ref="'InfiniteScrollItem' + item.infiniteScrollId" v-for="(item, index) in renderList" :key="item.infiniteScrollId + '_' + item.infiniteScrollKey">
<slot :item="item" :index="index" />
</div>
</div>
</div>
js部分
export default {
name: "Scroll",
data() {
return {
scrollHeight: 0, // 屏幕的可视区域高度
renderList: [], // 要渲染的的列表
listAll: [], // 总列表
scrollTop: 0, // 滚动了多少距离
listHeight: 0, // 容器内占位的高度计算
};
},
// 搭建成了组件 外部传入的数据
props: {
//数据列表
datas: {
default: () => {
return [];
},
type: Array,
},
//计算默认高度
defHeight: {
default: 50,
type: Number,
},
},
watch: {
// 传入的数据变化重新获取列表
datas(v) {
this.init();
},
},
mounted() {
this.getInfiniteScrollHeight();
this.init();
},
methods: {
// 获取可见区的高度
getInfiniteScrollHeight() {
if (document.querySelector(".InfiniteScrollContainer")) {
this.scrollHeight = document.querySelector(".InfiniteScrollContainer").offsetHeight;
}
},
init() {
if (this.datas.length > 0) {
this.listAll = JSON.parse(JSON.stringify(this.datas));
let height = 0;
this.listAll.forEach((i, index) => {
i.infiniteScrollId = index; // 存入当前行的下标
i.infiniteScrollKey = Date.parse(new Date()); // vue-for循环使用的唯一值
i.infiniteScrollTop = index === 0 ? 0 : this.listAll[index - 1].infiniteScrollTop + this.listAll[index - 1].infiniteScrollHeight; // 当前的行的距离列表顶部的距离
i.infiniteScrollHeight = this.defHeight; // 单行的高度
height += this.defHeight;
});
this.listHeight = height; // 列表总高
this.renderList = this.listAll.slice(0, 30); // 获取初始渲染的列表
}
},
scrollTops(e) {
// 滚动超出多少距离进行计算
if (Math.abs(e.target.scrollTop - this.scrollTop) > this.defHeight) {
this.scrollTop = e.target.scrollTop;
this.getRenderList();
}
},
getRenderList() {
let start = Math.floor((this.scrollTop - 300 > 0 ? this.scrollTop - 300 : 0) / this.defHeight); // 计算渲染的开始位置 滚动区域小于300不进行预渲染
let count = start + Math.ceil((this.scrollHeight + 600) / this.defHeight); // 添加了上下偏移的格300像素作为预渲染
this.renderList = this.listAll.slice(start, Math.min(count, this.listAll.length)); // 获取渲染的列表
},
// 插件修改后获取数据
getAllList() {
let list = [];
this.listAll.forEach((e) => {
let i = Object.assign({}, e);
delete i.infiniteScrollId;
delete i.infiniteScrollKey;
delete i.infiniteScrollTop;
delete i.infiniteScrollHeight;
list.push(i);
});
return list;
},
},
};
动态高度
上面是列表项固定高度的实现,而实际应用的时候,列表项的高度往往是由内容来决定。因此上面的方案就不适用了,要对上面的方案进行一点扩展。 展示了部分修改后的代码
获取单个的高度
init() {
if (this.datas.length > 0) {
this.listAll = JSON.parse(JSON.stringify(this.datas));
//.....
this.renderList = this.listAll.slice(0, 30); // 获取初始渲染的列表
this.$nextTick(() => {
this.renderList.forEach((i) => {
this.renderListHeightChange(i);
});
});
}
},
// 列表渲染出来 获取列表的真实高度
renderListHeightChange(v) {
if (!v.hasRenderDom) {
let id = v.infiniteScrollId;
let offsetHeight = this.$refs["InfiniteScrollItem" + id][0].offsetHeight;
this.listAll[id].infiniteScrollHeight = offsetHeight;
this.listAll[id].hasRenderDom = true;
// 重新统计下高度 防止
for (let i = 0; i < this.listAll.length; i++) {
if (i > 0) {
this.listAll[i].infiniteScrollTop = this.listAll[i - 1].infiniteScrollTop + this.listAll[i - 1].infiniteScrollHeight;
}
}
}
},
// 订单是否在可视区域
itemHasShow(item) {
if (item.infiniteScrollTop < this.scrollTop && item.infiniteScrollTop + item.infiniteScrollHeight > this.scrollTop + this.scrollHeight) {
return true;
} else if (item.infiniteScrollTop > this.scrollTop && item.infiniteScrollTop < this.scrollTop + this.scrollHeight) {
return true;
} else {
return item.infiniteScrollTop + item.infiniteScrollHeight > this.scrollTop - 300 && item.infiniteScrollTop + item.infiniteScrollHeight < this.scrollTop + this.scrollHeight + 300;
}
},
getRenderList() {
for (let i = 0; i < this.listAll.length; i++) {
if (this.itemHasShow(listAll[i])) {
let count = i + this.showCount; // 预估一个页面+预渲染的部分能最少展示多少条数据 prop传入
this.renderList = this.listAll.slice(i, Math.min(count, this.listAll.length)); // 获取渲染的列表
this.$nextTick(() => {
this.renderList.forEach((i) => {
this.renderListHeightChange(i);
});
});
break
}
}
},
列表的总高度要变成实时计算出来的
computed: {
// 容器内占位的高度计算
listHeight() {
let height = 0;
this.listAll.forEach((i) => {
height += i.infiniteScrollHeight;
});
return height;
},
},