需求
最近在写一个管理程序,有展示较多数据的可能,虽然实际数据量并不大,但是因为需要在IE中显示,网上找了几个虚拟列表组件虽然在Chrome中表现良好,但是换成IE11就卡的不行,因此需要对虚拟列表性能优化到极致。
实现方式
简单的说就是DOM里只放需要渲染的列表元素。具体的实现方式有两种:
-
基于Canvas自绘。
相当于完全放弃浏览器原有的绝大部分功能,虽然性能不错,但是需要造一大堆轮子(HTML,CSS,事件绑定...),作为一个懒人表示拒绝。
-
基于浏览器渲染引擎。
这个就简单多了,目前绝大多数选择的都是这种,这里也是。
实现细节
怎么计算位置之类的我就不多说了,直接看代码就好。我说下提升性能最关键的部分:
- 设置正确的
v-bind:key以复用组件。 - 把
items用Object.freeze()冻结。 onScroll里面调用window.requestAnimationFrame()节流。
接下来再说说与IE有关的操蛋事(不影响流畅度):
-
滚动时出现频闪。
拖动滚动条时的频闪没法解决。但是可以解决滚动鼠标滚轮时新出现的列表项闪烁的问题,办法是采用缓冲。IE11中我个人滑一次滚轮的滚动距离在300px左右,所以在代码中增加了
headBufferCount和tailBufferCount这两项。 -
IE中元素的最大高度限制比Chrome小,可能存在数据显示不完全的现象(即单个列表条目的高度越高,能显示的列表数量越少)。
这个问题存在是因为用了浏览器原生滚动条/滚动行为,理论上可以自己模拟个滚动条来解决。这个问题对要做的项目没什么影响,所以我并没有去管它。
代码
// App.vue
<template>
<div class="parent">
<div class="root">
<div class="head">
<div class="row head" :style="'height: ' + headHeight + 'px;'">
<template v-for="col in columns">
<div :class="'col ' + col[0]" :key="col[0]">{{col[0]}}</div>
</template>
</div>
</div>
<div class="body" :style="'height: ' + bodyHeight + 'px;'" @scroll.passive="body_onScroll">
<div class="blank" :style="'height: ' + fullHeight + 'px;'">
<div class="remain" :style="'top: ' + remainTop + 'px;'">
<template v-for="(item, k) in remainItems">
<template v-if="item">
<div :class="'row item ' + (selectedItems.indexOf(item.id) !== -1? 'selected': '')"
:style="'height: ' + itemHeight + 'px;'" :key="k" @click="item_onClick($event, item.id)">
<template v-for="col in columns">
<div :class="'col ' + col[0]" :key="col[0]">{{col[1](item, col[0])}}</div>
</template>
</div>
</template>
<template v-else>
<div class="row item" :style="'height: ' + itemHeight + 'px;'" :key="k"></div>
</template>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
scrollTop: 0,
items: Object.freeze(Array(1000000).fill().map((e, i) => (e, {id: i, A: 'A' + i, B: 'B' + i, C: 'C' + i, D: 'D' + i, E: 'E' + i, }))),
columns: Object.freeze([['A', (item, col) => item[col]],
['B', (item, col) => item[col]],
['C', (item, col) => item[col]],
['D', (item, col) => item[col]],
['E', (item, col) => item[col]]]),
selectedItems: [],
}
},
computed: {
fullHeight() {
return this.items.length * this.itemHeight;
},
remainTop() {
return (Math.ceil(this.scrollTop / this.itemHeight) - 1 - this.headBufferCount) * this.itemHeight;
},
remainCount() {
return parseInt((Math.min(this.items.length, Math.floor(this.bodyHeight / this.itemHeight)) + 1) + this.headBufferCount + this.tailBufferCount);
},
remainItems() {
let a = Array(this.remainCount).fill();
for(let i=0; i< a.length; i++) {
let itemIndex = Math.floor(this.scrollTop / this.itemHeight)+ i - this.headBufferCount;
if (!(this.scrollTop % this.itemHeight) ) {
itemIndex = Math.floor(this.scrollTop / this.itemHeight) - 1 + i - this.headBufferCount;
}
if (itemIndex >= 0 && itemIndex < this.items.length) {
a[i] = this.items[itemIndex];
}
}
return a;
},
itemHeight() {
return 20;
},
headHeight() {
return 30;
},
bodyHeight() {
return 270;
},
headBufferCount() {
return Math.round(300 / this.itemHeight);
},
tailBufferCount() {
return Math.round(300 / this.itemHeight);
}
},
methods: {
body_onScroll(event) {
let target = event.currentTarget;
if (!window["3fcff984-32a1-4d20-8c89-3e0c6d720cda"]) {
window.requestAnimationFrame(() => {
this.scrollTop = target.scrollTop;
window["3fcff984-32a1-4d20-8c89-3e0c6d720cda"] = false;
});
window["3fcff984-32a1-4d20-8c89-3e0c6d720cda"] = true;
}
},
item_onClick(event, itemId) {
event;
let index = this.selectedItems.indexOf(itemId);
if (-1 === index) {
this.selectedItems.push(itemId);
} else {
this.selectedItems.splice(index, 1);
}
}
}
}
</script>
<style scoped>
.parent {
overflow: hidden;
width: 500px;
height: 300px;
margin: 100px auto;
}
.root {
margin: 0;
padding: 0;
}
.body {
overflow: auto;
}
.blank {
overflow: hidden;
}
.remain {
position: relative;
overflow: hidden;
}
.row {
display: flex;
flex-direction: row;
overflow: hidden;
width: 500px;
}
.col {
flex-basis: 100px;
}
.selected {
background: red;
}
</style>
说明:保存成App.vue,执行vue serve即可。
提示:把1000000改成小一点的数(比如10000)再用IE打开。
效果
Chrome下,一百万数据量:

IE11下,十万数据量(只显示出七万多,见上文说明):

最后
其实更大数据量也是可以的,前提是电脑内存够( ̄▽ ̄)"。
第一次发文,欢迎大佬们提出宝贵意见。