在项目中若是能分页实现那最好不过了,不过不少时候长列表不可避免,这里又分两种状况javascript
下面就来讨论这两种状况如何进行优化,能够对比列表优化具体实现的源码来看 (注:下面是用 Vue 实现的,使用其余框架并不影响)html
实现的思路很简单就是根据滚动条是否滚动到底部(总高度 - 可见高度 - 滚动条高度),滚动到底部就添加新的数据java
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);
}
}
}
复制代码
<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;
}
}
复制代码
上面结构作了两件事情git
这里假设每一个列表的高度为 30px,剩下的部分就是计算出列表的整体高度
以及开始索引
和结束索引
,核心代码只有不到 10 行github
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>
复制代码
上面只最终的实现,实际上跟固定高度相比就是增长了获取索引的方法,固定高度咱们是知道对应子项的高度,因此能够经过可视高度来计算,而这里我用了随机数来设置高度,因此须要获取到对应的索引。 上面代码同时也作了两点优化,一是缓存,二是总高度优化less
总高度的实现有两种思路:dom
缓存则比较简单了,每次计算的时候把指针移动到计算的位置,同时将值添加上性能