某日某时某刻某分某秒,收到 小 A 同学 的消息,原因是他司有人反馈某项目中页面渲染内容太慢、太卡,且后端开发也贴出接口响应很快的日志,于是乎这个 优化
的小任务就落到了他头上。
经过简单询问得知:
-
页面上某个 table 组件 渲染的数据 不是分页的,接口将查到的所有符合的数据一股脑返回给了前端,约几万条数据
-
前端页面表现是 渲染慢、交互卡
模拟效果(渲染 3w 数据)如下:
治标不治本 — 滚动加载
当然 小 A 同学 很快就想到了自己实现滚动加载:
-
每次渲染20条数据,当滚动条 触底后继续渲染
于是马上进行提测,而测试同学也非常的敬业,一直滚动加载到了 几千条 数据,此时虽然在渲染表格项的时候没有出现卡顿,但是点击表格项时需要弹窗的这个交互,却又开始卡顿了,模拟效果如下(此处省略分批渲染
):
table 慢元素
由于 table
元素在渲染时需要 更多的计算资源,这其中需要计算表格的布局、单元格的大小和位置等,这可能会导致在 某些情况 下 table
元素的渲染速度较慢,因此 table
元素也叫 慢元素。
现在的问题显然由于使用 慢元素渲染大数据 而造成渲染卡顿、交互不流畅的问题,而前面的 分页加载 虽然可以解决 前期渲染卡顿 的问题,却不能解决 后期弹窗交互卡顿 的问题,原因就是 最后实际需要渲染的慢元素根本没有减少
。
那有什么办法能 保证每次实际渲染的数量不会递增 呢?
有,就是 只渲染可视区及其周边的数据
,而这也就是 虚拟列表
的核心。
虚拟列表
接下来我们会封装一个和虚拟列表相关的 hooks,不封装成组件的目的就是为了让此方法更加的通用,不局限外部使用的第三方组件或自己封装的组件,让其既支持 table 形式
,又让其支持普通的 list 形式
,还能让其支持 select 形式
。
虚拟列表 — 定高
要实现虚拟列表需要考虑如下三个方面:
-
滚动模拟
-
普通列表渲染
是可滚动
的,滚动产生的条件就是每次渲染数量会递增
,那么虚拟列表
就需要在保证每次渲染数量不递增
的情况下支持滚动
-
-
渲染正确的内容
-
保证用户在向上或向下滚动的过程中数据的
渲染内容是正确的
,只有这样看起来才和普通列表
表现一致
-
-
渲染的数据需要在可视区
-
虚拟列表
支持滚动之后,就需要保证渲染的数据一直存在于可视区
,而不是随着滚动到可视区之外
-
这里在引入三个名称和配图,方便进行理解,具体如下:
-
滚动容器
-
顾名思义,就是为了实现滚动,所以需要设置
height
固定高度 或 最大高度max-height
-
-
渲染实际高度的容器
-
为了实现模拟滚动,需要将实际高度的值,即 每个列表项高度之和 设置在某个元素上,这样就可以超过 滚动容器的高度,从而产生滚动效果
-
-
偏移容器
-
要实现渲染的数据始终处于可视区,那么可以针对 包裹着所有列表项的元素 进行处理,也就是将它的
transform: translateY(n)
值设置为 当前已滚动的高度scrollTop
即可 -
同时要保证每个滚动位置要渲染正确的数据,那么最简单的方式就是,根据 当前已滚动的高度
scrollTop
除以 单个列表项的高低 height,计算出当前需要渲染的 起始索引startIndex
,假设每次需要渲染20 条
数据,很容易算出 结束索引endIndex
,这样就可以知道当前滚动位置需要渲染的数据范围是什么
-
不到 100 行即可拥有虚拟滚动,具体实现如下:
// useVirtualList.ts
import { ref, onMounted, onBeforeUnmount, watch, computed} from "vue";
import type { Ref } from "vue";
interface Config {
data: Ref<any[]>; // 数据
itemHeight: number;// 列表项高度
size: number;// 每次渲染数据量
scrollContainer: string;// 滚动容器的元素选择器
actualHeightContainer: string;// 用于撑开高度的元素选择器
tranlateContaine