历时2年半,我造了一个基于 vue 的虚拟列表轮子?(stk-table-vue)

455 阅读7分钟

前言

这两年接触到的行情展示需求特别多,也渐渐积累整理并封装了一个用于行情展示表格。

因此,向大家介绍一个我封装的基于 sticky 的虚拟列表组件StkTableVue,及一点开发历程。

StkTableVue文档

Github

image.png

为什么

我也问过我自己,怎么放着这么多现成的表格组件不用,尤其对于vue生态来说,vxe-table已经是一个成熟的表格解决方案了,怎么还要要自己动手封装再造一个呢。

答:一开始也没有想要封装,最初的需求很简单,一个简单表格,虚拟列表,固定列都没有,出于包体积的考虑,我没有引入诸如vxe-table这样的组件,打算用原生table写了。

之后出现一个需求,让行或单元格更新的时候出现高亮动画,经过一番调研,我决定在我之前的表格中集成这个功能,然后慢慢迭代了两三年,表格的基本功能也都开始涉及到需求,组件渐渐封装起来了。

介绍

StkTableVued 的开发也参考了许多前辈,如vxe-tableant-design-vuenaive-ui等项目的表格组件的实现方案及接口设计,在此表示感谢。

下面介绍此表格的主要特点

sticky固定头,固定列

由于是基于 css 的 position: sticky 实现固定头与固定列,因此命名为 stk(sticky)-table-vue。

sticky 的实现也是参考与 数据表格 Data Table - Naive UI 的表格而来,其优点是dom结构简单,不需要再叠一个表格上去做固定列了,对减少总体的dom数量来说有积极意义,也不需要关联固定列表格的滚动状态。

且对于sticky实现固定列的表格来说,表格是一个整体,顺理成章支持滚动宽高的任意设置,就和原生表格一样。

我以前用过一些表格可能要设置scroll-x,scroll-y,感到不大方便。就比如:表格高度根据页面底部的剩余高度自适应,这类的需求,或需要js去动态计算高度。而现在,只需要正确使用 flex 就可以解决这类情况。

可以看看示例感受: 宽高 | StkTableVue文档

但正是因为使用 sticky 来做固定,在列阴影固定的z-index的实现中,有不小挑战。

最终经过各种磨难,实现了固定列,以及真正意义上的滚动吸附特性,查看如下demo。

固定列 | StkTableVue文档

z-index 示例

zIndex.png

虚拟列表

表格主要用在数据量大且需要更新的场景下。

前端渲染大量数据离不开虚拟列表。

对于canvas实现,性能没得说,但对于普遍开发的实用性来说,较不足,(不能正常写熟悉的html css了)。

先是支持了基本的纵向虚拟列表

之后发现需求里,极端情况下表头最多可以配置50多个字段,如果用户硬要展示这么多的字段,也会导致卡顿,数据延迟的问题,之后又实现了横向的虚拟列表,以保证尽量少的资源占用,仅渲染展示的在可视区的单元格。

虚拟列表 | StkTableVue文档

滚动白屏问题小计

虚拟列表存在千古难题,就是在某些浏览器下滚动快了会白屏,和浏览器版本有关系,还可能和系统是否有显卡有关系。

naive ui 的表格通过自己实现了一个滚动条组件,成功解决了这个问题。

但目前,我仅对其vxe-table 的实现,也就是大众方案。额外通过onwheel 事件触发滚动,解决白屏,但拖动原生滚动条还是会白屏。

白屏是浏览器对滚动做了特殊的优化。使滚动的事件不被渲染阻塞。这样就导致快速滚动到下面时,表格的内容还没有渲染出来,因为用户滚动事件优先级最高了。

但这个滚动事件只要被代理(通过js触发滚动)就不会被浏览器优化,也就不会出现白屏。

比如 vxe-table,鼠标悬浮在固定列中滚动,实际滚动的是覆盖在主表上面的子表格,同时通过js触发整个表格的滚动,则底层的主表格滚动就不会白屏,而固定列的表格则依旧会白屏。

数据更新高亮

image.png

高亮行、单元格 | StkTableVue文档

其数据如果发生更新,需要高亮当前行或者单元格。

这个需求,如果不放在虚拟列表中直接用 css 的帧动画就可以实现了。但在虚拟列表中就不一样了。由于虚拟列表只展示可视区的内容,如果当前正在高亮的行,不在当前的可可视区内,用css是选择不到的。

js方案

对于这个问题,一开始我的方案是用 d3-interpolate 来根据时间计算颜色值,并保存到每一行的对象中。等待进入可视区后,再根据时间去推断动画的进度。

可是这个方案的问题在于,js的计算频率较多,支撑不起庞大的数据推送高亮。说白了就是性能不够理想。

css方案

css 方案由于不易控制动画的进度,在普通表格中可以使用。

由于css的动画 animation + @keyframe 动画一般从设定的from开始。

假设我设定背景颜色透明度从 1 渐变到 0 。假如此行在第二页中,等待时间到50%了,再进入到可视区,此时如果给这行再加上一个class触发帧动画,则帧动画会再从1开始。

这里需要控制动画在正确的进度开始,就不是那么好弄了。

animation api 方案

Element.animate() - Web API | MDN

这个api就适合这个场景了,他可以直接改变一个元素的动画效果,而不借助css触发。且兼容性较好。

image.png

而且我发现 animate(el,option)option.iterationStart 可以设置动画开始的时刻,再配合 option.iterations 配置动画的持续时间。

这样就能在动画执行到中途时进入可视区,仍然可以正常执行剩余时间的动画效果了。

配置式自定义单元格

参考了ant-design-vue 中的自定义单元格,我在使用的过程中发现,相较于旧版 vxe-table(新版好像也支持配置式了),配置很多的 的插槽来说,在columns中配置自定义单元格的实现还是蛮优雅的。

因此StkTableVue 也使用了配置式的单元格接口。

import MyCell from './MyCell.vue';
const columns = [{
    // ...
    customCell: MyCell
}]

参考: 自定义单元格 | StkTableVue文档

vue2 支持

近两年来,部门的前端基础也从 vue2 换成 vue3了,对于已上线的vue2项目,升级成vue3 成本较大。为此在组件的开发之初就想要同时支持vue2 和 vue3,以支持旧项目的迭代。

在vue2.7 未正式发布前,源码使用vue2的 optional Api 实现,以同时支持vue2和vue3。

在vue2.7 公布后,又将其重构为 composition Api 以及若干hooks。

image.png

PS: hooks较optional api 总体来说要优雅多了😂。

vue2 现在可以通过引入源码的方式使用,环境要求是 vue SFC + ts

在vue2.7 中使用 | StkTableVue文档

vue2 滚动优化

由于vue2 虚拟dom 的 diff 机制(双端diff)原因。在vue2中,虚拟列表向下滚动时,表格所有的tr都会更新。 向上滚动时则在表格最上方新增tr,表格末尾删除tr,这样才是正确的。

向下滚动在体感上会明显比向上滚动要卡一点。

因此,为这种情况进行优化,在用鼠标滚轮滚动时,向上与向下滚动的体感保持一致。

具体:Vue2 滚动优化 | StkTableVue

实战demo

大量数据实战demo

性能优化笔记(不在demo里)

大量数据需要各种细微的优化。比如

  • 数据使用shallowRef包裹。
  • 新数据插入使用二分插入。
  • 一帧时间内推送的数据结合起来,批量更新。
  • 表格配置高亮帧率fps,降低到合适帧率。

结尾

表格目前已经上线使用了10来个项目,也在持续维护中。

如果本文对您有帮助,希望帮忙在github支持一下`🙏🙏🙏 github.com/ja-plus/stk…