「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
前言
对于前端页面来说,用户体验永远是排在第一位的,只有用户体验提升了,别人才会愿意去使用、去推荐你的产品。而 长列表 就是必不可少的优化项目之一。通常,像表格之类的长列表项,都是通过分页来提升页面速度。对于下拉选项那种情况,虽然也可以通过前端监听滚动条分片加载,但加载多了之后还是避免不了出现卡顿等情况。今天文章就是要介绍另外一种丝滑般的体验 -- 虚拟滚动列表。
正题
首先话不多说,先上效果对比图。这里是使用了 vue-virtual-scroll-list 这个库来优化,并使用 element 中的穿梭框组件 transfer 作为例子,也是我实际项目中遇到的问题。看图:
优化之前: (大概生成了7000条数据,从动图中可以看到,无论是进行过滤或者是勾选复选框,响应速度极其慢,滚动也不是很流畅)
优化之后:(可以看到,优化之后,无论是搜索还是复选框都响应都非常快,滚动更是吃了德芙一般丝滑)
原理探究
虚拟滚动这个词听着有点高大上,其实原理并不复杂。首先我们知道,造成页面卡顿响应慢的原因是页面上一次性渲染了太多的 DOM 元素。所以只需要减少页面上的渲染的元素不就行了,那该怎么减少呢?
虽然后端接口返回的数据可能有成千上万条,但最终呈现给用户,用户所看到的就只是容器内的一部分数据,仔细想想是不是可以只渲染用户看到的部分,其他还没滚动到的数据就不渲染了? 虚拟滚动列表 就是这样一个原理。看图:
假设知道每个列表项的高度,我们就可以很容易计算出可滚动的区域有多大,当用户开始滚动时,监听滚动事件,获取滚动的值 scrollTop,根据每个列表项的高度就可以计算出当前应该展示数据项的范围,并且根据这个范围计算出 paddingTop 和 paddingBottom ,使滚动条处于一个正确的位置。
改造穿梭框
接下来还是以开头那个为例子,el-transfer 配合 vue-virtual-scroll-list 来达到优化长列表的目的。我这边是直接复制了 transfer 组件出来稍微改造下成为一个新的组件。
先看下 vue-virtual-scroll-list 的使用方法
// 根组件
<virtual-list style="height: 360px; overflow-y: auto;" // make list scrollable
:data-key="'uid'"
:data-sources="items"
:data-component="itemComponent"
/>
<script>
import Item from './Item'
import VirtualList from 'vue-virtual-scroll-list'
export default {
name: 'root',
data () {
return {
itemComponent: Item,
items: [{uid: 'unique_1', text: 'abc'}, {uid: 'unique_2', text: 'xyz'}, ...]
}
},
components: { 'virtual-list': VirtualList }
}
</script>
// 渲染的每一项的item组件
<template>
<div>{{ index }} - {{ source.text }}</div>
</template>
<script>
export default {
name: 'item-component',
props: {
index: { // index of current item
type: Number
},
source: { // here is: {uid: 'unique_1', text: 'abc'}
type: Object,
default () {
return {}
}
}
}
}
</script>
可以看出, data-component 需要传递一个组件,来制定你每一项应该怎么渲染,下面开始改造。
下图是改造后的组件,主要是修改了 transfer-panel 里面的内容以及新增了 item 组件,这个 item 组件就是待会要传给 data-component 的
首先看下 transfer-panel 文件,主要改动 27~40 行:
// 这是原来的
<el-checkbox-group
v-model="checked"
v-show="!hasNoMatch && data.length > 0"
:class="{ 'is-filterable': filterable }"
class="el-transfer-panel__list">
<el-checkbox
class="el-transfer-panel__item"
:label="item[keyProp]"
:disabled="item[disabledProp]"
:key="item[keyProp]"
v-for="item in filteredData">
<option-content :option="item"></option-content>
</el-checkbox>
</el-checkbox-group>
// 修改成如下
<el-checkbox-group
v-show="!hasNoMatch && data.length > 0"
v-model="checked"
:class="{ 'is-filterable': filterable }"
class="el-transfer-panel__list"
>
<virtual-list
:data-key="keyProp"
:data-sources="filteredData"
:data-component="itemComponent"
:extra-props="{ showOverflowTooltip: showOverflowTooltip }"
style="height: 100%; overflow-y: auto"
/>
</el-checkbox-group>
把中间的渲染的项目替换成我们 virtual-list 组件,由它来控制,接着看下 item 文件里的内容,其实不多,只是把原来代码中的 optionContent 搬了过来。
// item
<script>
export default {
name: 'ItemDetail',
components: {
OptionContent: {
props: {
option: Object
},
render(h) {
const getParent = (vm) => {
if (vm.$options.componentName === 'ElTransferPanel') {
return vm
} else if (vm.$parent) {
return getParent(vm.$parent)
} else {
return vm
}
}
const panel = getParent(this)
const transfer = panel.$parent || panel
return panel.renderContent ? (
panel.renderContent(h, this.option)
) : transfer.$scopedSlots.default ? (
transfer.$scopedSlots.default({ option: this.option })
) : (
<span>
{this.option[panel.labelProp] || this.option[panel.keyProp]}
</span>
)
}
}
},
props: {
source: { // source是 vue-virtual-scroll-list 自动传给每项组件的
type: Object,
default() {
return {}
}
}
},
render(h) {
const getParent = (vm) => {
if (vm.$options.componentName === 'ElTransferPanel') {
return vm
} else if (vm.$parent) {
return getParent(vm.$parent)
} else {
return vm
}
}
const panel = getParent(this)
return (
<el-checkbox
label={this.source[panel.keyProp]}
class="el-transfer-panel__item"
>
<option-content option={this.source}></option-content>
</el-checkbox>
)
}
</script>
这里看源码的时候学习到了个小技巧,有些可能局部特殊用的小组件,可以直接写在 compoments 里面,不同于模板文件的是,要用 jsx 来渲染。
还有一个 getParent 方法,可以从本组件位置开始,往上找到指定组件名的父组件,就可以取到父组件里面相关的一些值来用。
以上就是将 el-transfer 穿梭框改造成支持虚拟列表滚动的过程,建议大家可以试试。
拓展
有时在想,有没有一种 css 属性可以直接做到这种效果呢,那会方便很多。其实,还真有。在 Chromium 85 中,增加了 content-visibility 属性,可跳过不在屏幕上的内容渲染,包括布局和渲染,直到真正需要布局渲染的时候为止。所以利用它可以使初始用户加载速度更快,还能与屏幕上的内容进行更快的交互。但是,兼容性,你们懂的 = =。
感兴趣的可以去了解了解这个属性