长列表优化
bosens-China opened this issue · 0 comments
bosens-China commented
前言
在项目中如果能分页实现那最好不过了,不过很多时候长列表不可避免,这里又分两种情况
- 第一次不用全部加载完成,这种可以使用懒加载或者说无限滚动的方式来实现
- 另外一种则是一次要渲染全部数据出现
下面就来讨论这两种情况如何进行优化,可以对比列表优化具体实现的源码来看
(注:下面是用 Vue 实现的,使用其他框架并不影响)
无限滚动
实现的思路很简单就是根据滚动条是否滚动到底部(总高度 - 可见高度 - 滚动条高度),滚动到底部就添加新的数据
function scroll({ target }) {
const DISTANCE = 40;
const h = target.scrollHeight - (target.clientHeight + target.scrollTop);
if (h < DISTANCE) {
for (let i = 0, j = this.list.length, l = this.list.length; i < l; i++) {
this.list.push(j + i);
}
}
}
虚拟列表
引用一张图,可以看见我们实现的思路就是只渲染可见部分的列表,每次滚动条变化的时候更改展示的列表,在下面的演示中,我们都会用到一个基础的 html 结构,这里先贴一下
<div class="root">
<div class="container"></div>
<ul class="content">
<li class="item" v-for="item of nowList" :key="item.value">
{{ item.value }}
</li>
</ul>
</div>
.root {
border: 1px solid #999;
list-style: none;
overflow: auto;
height: 400px;
position: relative;
.container {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.content {
.container();
z-index: 1;
margin: 0;
padding: 0;
list-style: none;
}
.item {
border-bottom: 1px solid #ccc;
padding-left: 40px;
}
}
上面结构做了两件事情
- 固定总列表的高度,让其出现滚动条
- 用一个遮罩 div 撑起整个列表的高度
固定
这里假设每个列表的高度为 30px,剩下的部分就是计算出列表的总体高度
以及开始索引
和结束索引
,核心代码只有不到 10 行
scroll() {
const dom = this.$refs.root;
const total = Math.ceil(dom.clientHeight / this.height);
const start = Math.floor(dom.scrollTop / this.height);
const end = start + total;
this.start = start;
this.end = end;
}
总索引: 当前视图的高度 / 子项的高度,不过注意需要向上取整;
开始索引: 滚动的距离 / 子项的高度
结束索: 总索引 + 开始索引
下面是完整的代码
<template>
<div>
<div class="root" ref="root" @scroll="scroll">
<div class="container" :style="{ height: totalHeight }"></div>
<ul class="content" :style="{ transform: getTransform }">
<li
class="item"
:style="{ height: height + 'px', lineHeight: height + 'px' }"
v-for="(item, i) of nowList"
:key="i"
>
{{ item }}
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
data() {
return {
list: Array(10000)
.fill(1)
.map((f, i) => i),
height: 30,
start: 0,
end: 0
};
},
computed: {
totalHeight() {
return this.height * this.list.length + "px";
},
nowList() {
return this.list.slice(this.start, this.end);
},
getTransform() {
return `translate3d(0,${this.start * this.height}px,0)`;
}
},
mounted() {
this.scroll();
},
methods: {
scroll() {
const dom = this.$refs.root;
const total = Math.ceil(dom.clientHeight / this.height);
const start = Math.floor(dom.scrollTop / this.height);
const end = start + total;
this.start = start;
this.end = end;
}
}
};
</script>
<style lang="less" scoped>
.root {
border: 1px solid #999;
list-style: none;
overflow: auto;
height: 400px;
position: relative;
.container {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.content {
.container();
z-index: 1;
margin: 0;
padding: 0;
list-style: none;
}
.item {
border-bottom: 1px solid #ccc;
padding-left: 40px;
}
}
</style>
非固定
非固定需要考虑的更多则是性能的问题,下面先贴一个完整的代码,在需要说明部分已经注释了
<template>
<div>
<div class="root" ref="root" @scroll="scroll">
<div class="container" :style="{ height: totalHeight }"></div>
<ul class="content" :style="{ transform: getTransform }">
<li
class="item"
v-for="item of nowList"
:style="{
height: item.height + 'px',
lineHeight: item.height + 'px'
}"
:key="item.value"
>
{{ item.value }}
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
data() {
return {
list: Array(10000)
.fill(1)
.map((f, i) => {
return {
value: i,
height: this.getRandom(10, 100)
};
}),
start: 0,
end: 0,
// 指针
pointer: -1,
// 缓存
cache: {},
// 初始总数
initialHeight: 50
};
},
computed: {
totalHeight() {
// 这里是获取总体高度,判断了两种情况,第一种是给定初始总数,另外一种则是没有,如果没有的话,高度就是已缓存的 + 未缓存的部分
if (this.initialtotal >= 0) {
const { top, height } =
this.pointer >= 0
? this.getIndexOffset(this.pointer)
: { top: 0, height: 0 };
return `${top +
height +
(this.list.length - 1 - this.pointer) * this.initialHeight}px`;
}
const { height } = this.list.reduce(function(x, y) {
return {
height: x.height + y.height
};
});
return height + "px";
},
// 可视数据
nowList() {
return this.list.slice(
this.start,
Math.min(this.end + 1, this.list.length)
);
},
getTransform() {
return `translate3d(0,${this.getIndexOffset(this.start).top}px,0)`;
}
},
mounted() {
this.scroll();
},
methods: {
// 滚动事件
scroll() {
const dom = this.$refs.root;
// 获取索引
const start = this.getIndex(dom.scrollTop);
// 把当前可视的高度 + 滚动条的高度,再去取索引
const end = this.getIndex(dom.scrollTop + dom.clientHeight);
this.start = start;
this.end = end;
},
// 取出指定范围随机数
getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
},
// 根据滚动条y获取指定坐标
getIndex(scrollTop) {
// 判断思路很简单,如果高度大于滚动条肯定就出现了,另外一种则是判断了边界问题
let total = 0;
for (let i = 0, j = this.list.length; i < j; i++) {
if (total >= scrollTop || j - 1 === i) {
return i;
}
// 这里主要是起缓存作用的
total += this.getIndexOffset(i).height;
}
return 0;
},
// 获取指定坐标的位置和高度
getIndexOffset(index) {
// 如果存在缓存中直接返回
if (this.pointer >= index) {
return this.cache[index];
}
let total = 0;
// 这里是为了比较没有取到的情况
if (this.pointer >= 0) {
const li = this.cache[this.pointer];
total = li.top + li.height;
}
// 注意上面因为取的值是li.top + li.height,所以i从 + 1开始
for (let i = this.pointer + 1; i <= index; i++) {
const size = this.list[i].height;
this.cache[i] = {
top: total,
height: size
};
total += size;
}
if (index > this.pointer) {
this.pointer = index;
}
return this.cache[index];
}
}
};
</script>
<style lang="less" scoped>
.root {
border: 1px solid #999;
list-style: none;
overflow: auto;
height: 400px;
position: relative;
.container {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.content {
.container();
z-index: 1;
margin: 0;
padding: 0;
list-style: none;
}
.item {
border-bottom: 1px solid #ccc;
padding-left: 40px;
}
}
</style>
上面只最终的实现,实际上跟固定高度相比就是增加了获取索引的方法,固定高度我们是知道对应子项的高度,所以可以通过可视高度来计算,而这里我用了随机数来设置高度,所以需要获取到对应的索引。
上面代码同时也做了两点优化,一是缓存,二是总高度优化
总高度的实现有两种思路:
- 计算所有的高度,这种实际上有点浪费性能;
- 给定一个大概的值,拿缓存的值 + 没有缓存的值,没有缓存的值就是对应数据的长度 - 已缓存的坐标,之后每次缓存变化的时候再计算;
缓存则比较简单了,每次计算的时候把指针移动到计算的位置,同时将值添加上