在項目中若是能分頁實現那最好不過了,不過不少時候長列表不可避免,這裏又分兩種狀況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
緩存則比較簡單了,每次計算的時候把指針移動到計算的位置,同時將值添加上性能