前端分享--虚拟列表在el-table中的实现(新增vue3版本)

4,664 阅读5分钟

前言

在工作中,有时会遇到需要一些不能使用分页方式来加载列表数据的业务情况,对于此,我们称这种列表叫做长列表。比如,在一些外汇交易系统中,前端会实时的展示用户的持仓情况(收益、亏损、手数等),此时对于用户的持仓列表一般是不能分页的。本文会介绍使用虚拟列表的方式,来同时加载大量数据。

为什么需要使用虚拟列表

假设我们的长列表需要展示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中执行的

关于Event Loop的详细内容请参见这篇文章-->

然后,我们通过ChromePerformance工具来详细的分析这段代码的性能瓶颈在哪里:

Performance可以看出,代码从执行到渲染结束,共消耗了960.8ms,其中的主要时间消耗如下:

  • Event(click) : 40.84ms
  • Recalculate Style : 105.08ms
  • Layout : 731.56ms
  • Update Layer Tree : 58.87ms
  • Paint : 15.32ms

从这里我们可以看出,我们的代码的执行过程中,消耗时间最多的两个阶段是Recalculate StyleLayout

  • Recalculate Style:样式计算,浏览器根据css选择器计算哪些元素应该应用哪些规则,确定每个元素具体的样式。
  • Layout:布局,知道元素应用哪些规则之后,浏览器开始计算它要占据的空间大小及其在屏幕的位置。

在实际的工作中,列表项必然不会像例子中仅仅只由一个li标签组成,必然是由复杂DOM节点组成的。

那么可以想象的是,当列表项数过多并且列表项结构复杂的时候,同时渲染时,会在Recalculate StyleLayout阶段消耗大量的时间。

虚拟列表就是解决这一问题的一种实现。

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
})

image.png 在拿到数据之后对数据做截断处理

image.png 最后效果图

image.png 从图中看页面始终只有10个tr 表格的高度是由i标签撑起来的 滚动的时候通过监听事件只替换这10个tr的值

elementPlus的table实现虚拟列表功能(vue3)

elementPlus+vue3的版本 有一些变化,不过整体思路不变

image.png

在拿到列表数据之后,对数据做截断处理并放入虚拟表格中

image.png

保证表格中只会显示滚动条对应位置的数据 image.png

渲染后的表格结构为,可以看到页面也是始终只有10个tr 表格的高度是由i标签撑起来的 滚动的时候通过监听事件只替换这10个tr的值:

image.png