在有些需求场景中,我们可能需要渲染大量列表项,同时分页加载可能不是理想的加载方式。
大量的项目 layout 会导致页面性能下降,因为浏览器需要计算每个元素的位置,这个计算是非常昂贵的。同时,大量的 DOM 元素也会占用大量的内存,导致页面加载变慢。
这时候就需要用到长列表优化技术以优化渲染开销,虚拟列表就是其中一种。
虚拟列表是一种只渲染可视区域的列表,当用户滚动列表时,只渲染可视区域(也可以包含即将可视的缓冲区)的列表项,这样可以大大减少渲染开销。每当用户滚动列表时,虚拟列表会根据滚动位置动态调整列表项显示的内容,对用户来说就像是正常滚到了下一个项目,但实际上是列表向下平移且显示的内容换了一批。
下面这个 demo 可以展示虚拟列表的效果:左边是虚拟列表,右边是真实列表,当关闭动画之后,左右两边的列表对用户来说体验是一样的。而打开动画之后,可以看到每当用户滚动列表的高度超过一个列表项之后,页面实际上渲染的仍然是同一批 DOM 元素,只是根据滚动位置动态调整了显示的内容以及位置。
第三个列表项的样式不同,可以更好的理解虚拟列表的原理
更改已经渲染在页面的 DOM 元素的内容和样式相比于创建新的 DOM 元素要快得多,因为浏览器不需要重新计算元素的位置,这就是虚拟列表的优势所在。
下面分享一个简易虚拟列表的 Vue 代码及其注释。简单来说就是通过监听滚动事件,根据滚动位置动态调整列表项显示的内容,以及位置。
<template> <div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)"> <div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }" ></div> <!-- infinite-list-phantom 作为容器,高度始终是最大高度,用于提供滚动条 --> <!-- 内部 list 的定位相对于容器, 距离 top 为 0 保证 transform 有效 --> <!-- list 一共只有常数个项目,保证屏幕显示完全即可--> <!-- scroll 事件发生时,容器正常滚动, 而 list 则计算应该平移的高度并平移 --> <!-- 一旦第一个 item 滚出屏幕,就会让 list 向下平移 item 的高度,同时会更新显示的内容 --> <!-- 这样对用户来说就像是正常滚到了下一个项目,但实际上是列表向下平移且显示的内容换了一批 --> <div class="infinite-list" :style="{ transform: getTransform }"> <div ref="items" class="infinite-list-item" v-for="item in visibleData" :key="item.id" :style="{ height: itemSize + 'px', lineHeight: itemSize + 'px' }" > {{ item.value }} </div> </div> </div> </template> <script> export default { name: "VirtualList", props: { //所有列表数据 listData: { type: Array, default: () => [], }, //每项高度 itemSize: { type: Number, default: 200, }, }, computed: { //列表总高度 listHeight() { return this.listData.length * this.itemSize; }, //可显示的列表项数 visibleCount() { return Math.ceil(this.screenHeight / this.itemSize); }, //偏移量对应的style getTransform() { return `translate3d(0,${this.startOffset}px,0)`; }, //获取真实显示列表数据 visibleData() { return this.listData.slice( this.start, Math.min(this.end, this.listData.length) ); }, }, mounted() { this.screenHeight = this.$el.clientHeight; this.start = 0; this.end = this.start + this.visibleCount; }, data() { return { //可视区域高度 screenHeight: 0, //偏移量 startOffset: 0, //起始索引 start: 0, //结束索引 end: null, }; }, watch: { startOffset() { console.log(this.startOffset); }, }, methods: { scrollEvent(e) { e.preventDefault(); //当前滚动位置 let scrollTop = this.$refs.list.scrollTop; //此时的开始索引 this.start = Math.floor(scrollTop / this.itemSize); //此时的结束索引 this.end = this.start + this.visibleCount; //此时的偏移量 this.startOffset = scrollTop - (scrollTop % this.itemSize); }, }, }; </script> <style scoped> .infinite-list-container { height: 100%; overflow: auto; position: relative; -webkit-overflow-scrolling: touch; } .infinite-list-phantom { position: absolute; left: 0; top: 0; right: 0; z-index: -1; } .infinite-list { left: 0; right: 0; top: 0; position: absolute; text-align: center; } .infinite-list-item { padding: 10px; color: #555; box-sizing: border-box; border-bottom: 1px solid #999; } </style>
性能提升:
提高响应速度:
节省带宽:
可扩展性强:
实现复杂:
浏览器兼容性问题:
transform
、will-change
)和 JavaScript 特性,导致兼容性问题。SEO问题:
用户体验:
代码复杂度增加: