前言
在工作中,有时会遇到需要一些不能使用分页方式来加载列表数据的业务情况,对于此,我们称这种列表叫做长列表
。比如,在一些外汇交易系统中,前端会实时的展示用户的持仓情况(收益、亏损、手数等),此时对于用户的持仓列表一般是不能分页的。本文会介绍使用虚拟列表
的方式,来同时加载大量数据。
为什么需要使用虚拟列表
假设我们的长列表需要展示10000条记录,我们同时将10000条记录渲染到页面中,先来看看需要花费多长时间:
<button id="button">button</button><br>
<ul id="container"></ul>
document.getElementById('button').addEventListener('click',function(){
// 记录任务开始时间
let now = Date.now();
// 插入一万条数据
const total = 10000;
// 获取容器
let ul = document.getElementById('container');
// 将数据插入容器中
for (let i = 0; i < total; i++) {
let li = document.createElement('li');
li.innerText = (Math.random() * total)
ul.appendChild(li);
}
console.log('JS运行时间:',Date.now() - now);
setTimeout(()=>{
console.log('总运行时间:',Date.now() - now);
},0)
// print JS运行时间: 38
// print 总运行时间: 957
})
当我们点击按钮,会同时向页面中加入一万条记录,通过控制台的输出,我们可以粗略的统计到,JS的运行时间为38ms
,但渲染完成后的总时间为957ms
。
简单说明一下,为何两次console.log
的结果时间差异巨大,并且是如何简单来统计JS运行时间
和总渲染时间
:
- 在 JS 的
Event Loop
中,当JS引擎所管理的执行栈中的事件以及所有微任务事件全部执行完后,才会触发渲染线程对页面进行渲染 - 第一个
console.log
的触发时间是在页面进行渲染之前,此时得到的间隔时间为JS运行所需要的时间 - 第二个
console.log
是放到 setTimeout 中的,它的触发时间是在渲染完成,在下一次Event Loop
中执行的
然后,我们通过Chrome
的Performance
工具来详细的分析这段代码的性能瓶颈在哪里:
从Performance
可以看出,代码从执行到渲染结束,共消耗了960.8ms
,其中的主要时间消耗如下:
- Event(click) :
40.84ms
- Recalculate Style :
105.08ms
- Layout :
731.56ms
- Update Layer Tree :
58.87ms
- Paint :
15.32ms
从这里我们可以看出,我们的代码的执行过程中,消耗时间最多的两个阶段是Recalculate Style
和Layout
。
Recalculate Style
:样式计算,浏览器根据css选择器计算哪些元素应该应用哪些规则,确定每个元素具体的样式。Layout
:布局,知道元素应用哪些规则之后,浏览器开始计算它要占据的空间大小及其在屏幕的位置。
在实际的工作中,列表项必然不会像例子中仅仅只由一个li标签组成,必然是由复杂DOM节点组成的。
那么可以想象的是,当列表项数过多并且列表项结构复杂的时候,同时渲染时,会在Recalculate Style
和Layout
阶段消耗大量的时间。
而虚拟列表
就是解决这一问题的一种实现。
elementui的table实现虚拟列表功能
elementui的table组件本身并不支持虚拟列表功能,在对付上100条数据数十列的情况下渲染就很慢,对列表的各种操作都慢,比如点个编辑打开弹窗等等,肉眼可见的延迟。数据越多这样的卡顿越是明显。最近在做的项目就有用到elementui,80%的页面都是各种表格,有的表格要求最低行数100,最高1000,列最少15列,最多达20多列,渲染、操作各项体验非常难受。 所以,虚拟列表势在必行。
明确思路
对于超长的表格,在请求到数据之后,先取15条出来交给table组件渲染。剩下的,在滚动列表个时候监听一下表格的滚动条,在逐步添加数据,暴露在视野中的就渲染,其他直接false掉。这里有一个问题需要解决,就是初始下的15条并不能给表格带来符合100条的滚动高度,这里提供两种办法:
- 使用一个标签,设置高度为总长度的高 单行的高 * 当前页总条数
- 不使用标签,直接使用伪元素,给伪元素设置高 单行的高 * 当前页总条数
在改变每页条数的情况下,需要修改这个高度,考虑到js不容易修改伪元素的高度,所以采用真实标签的做法。
具体实现
在进入__mounted__生命周期时,对__table组件初始化__。
let i = document.createElement('i')
i.id = 'vheight'
i.style.width = '0'
i.style.float = 'right'
document.querySelector('.el-table__body-wrapper').append(i)
document.querySelector('.el-table__body').style.float = 'left'
this.tableData2 = {} //tableData2不需要在页面显示,故不放入data中
同时,对表格的滚动条添加监听事件
document.querySelector('.el-table__body-wrapper').addEventListener('scroll', () => {
//xxx
})
在拿到数据之后对数据做截断处理
最后效果图
从图中看页面始终只有10个tr 表格的高度是由i标签撑起来的 滚动的时候通过监听事件只替换这10个tr的值
elementPlus的table实现虚拟列表功能(vue3)
elementPlus+vue3的版本 有一些变化,不过整体思路不变
在拿到列表数据之后,对数据做截断处理并放入虚拟表格中
保证表格中只会显示滚动条对应位置的数据
渲染后的表格结构为,可以看到页面也是始终只有10个tr 表格的高度是由i标签撑起来的 滚动的时候通过监听事件只替换这10个tr的值: