”旧“活旧整~一行行实现表格虚拟滚动

1,771 阅读4分钟

最终效果

前言

恰逢周末~ 早上起床,拥抱过☀️太阳,于是开始做家庭作业~

众所周知的一道面试题:如何处理后端返回的10w+数据 ? 对于这种问题很多人一听就想抡起锤子🔨,扬言锤爆后端,但是还别说,真的会有这样场景,如处理Excel表格、做大数据分析,对数据库数据进行管理,不说有10w+数据,但是若前端直接渲染,这种量级还是足以让浏览器卡顿,直至卡死(可以看我源码中的.csv文件)。

因此面对这个问题应该从几个方面思考:

  1. 服务端的问题,优先从服务端着手,后端能否对数据进行分页呢?

  2. 从服务端思考,后端若不处理,前端能否做个node中间层去处理这些数据?

  3. 最后,前端只能祭出杀手锏,虚拟滚动!

什么是虚拟滚动

场景

通常,在H5中,列表页的展示可谓家常便饭,如果列表数据量很大又无限下拉,这时候大量dom的渲染将导致页面卡顿;而在PC端上,若是大数据往往都是通过表格进行渲染,或者一个大的tree进行渲染,例如使用Element UI 的 tree组件,一旦需要渲染量级稍微多点的数据,tree组件会递归的生成dom然后渲染到页面,从而导致页面的卡顿。

特点

知道了场景,自然就衍生出了较为通用的解决方案——虚拟滚动,虚拟滚动就是只渲染当前视口的dom元素(当然也可以渲染3页作为buff缓存,让用户体验更好~),虚拟滚动核心就是按需渲染,不在可视区域的元素不需要渲染,因此也叫可视区域渲染。特点如下:

  1. 滚动容器:像window窗口就是可以滚的,也可通过overflow通过布局的方式实现滚动,从而通过onScroll事件对容器里面的元素进行滚动监听,就可知晓元素的相对于容器的位置。
  2. 可滚动区域: 容器内部可以滚动的区域是多少,比如有1000个元素,width是60,那么可滚动的区域就是 1000*60 px。
  1. 可视区域: 看得见的区域,比如H5中屏幕大小,浏览器的视觉窗口大小。

虚拟滚动实现

因此虚拟滚动的原理就是:当用户在可滚动区域滑动时,容器可通过监听知道scrollTopscrollLeft的变化,从而可以知道当前那些元素应该渲染到可视区域。

则实现一个支持横向虚拟滚动的表格组件的思路如下:

  1. 计算可视区域的 width
  2. 依据 scrollLeft 推算出可视区域显示哪几列;
  1. 在滚动时,实时监听滚动计算第2步;
  2. 计算出表格内容的总宽度,通过transform隐藏未渲染部分

直接上代码:github.com/AutumnWhj/v…

表格组件—横向虚拟滚动实现

明确最终调用

表格可分为表头跟表格主体的数据部分,即header跟body,因此表格组件的接口可以如下:里面根据数据去实现自有逻辑。

<acho-virtual-table
  :columns="columns"
  :dataSource="dataSource"
/>

可滚动table布局

按虚拟滚动的特点,可以做出以下布局,布局合理了就成功了一大半,后面需要做的无法就是对滚动时的监听做些许的计算。

计算窗口可渲染的列-columns

要计算可视窗口的列,得依据scrollLeft,即相对滚动容器,窗口往右滚动的距离去计算哪一列可以被看到,如下:

handleScrollLeft(event) {
  window.requestAnimationFrame(() => {
    const scrollLeft = event.target.scrollLeft
    let start = Math.floor(scrollLeft / this.itemWidth)

    this.start = Math.max(start, 0)
    this.end = this.start + this.visibleCount + this.buffSize
    // 更新偏移量
    const startOffset = scrollLeft - (scrollLeft % this.itemWidth)
    this.transform = `translate3d(${startOffset}px,0,0)`
  })
},

start的计算通过scrollLeft除每一列的宽度并向下取整,end则是可视区域的width(clientWidth) 除 每一列的宽度 并向上取整,知道了startend两个指针之后,就可以对表头列数据columns进行截取了。

在vue中使用computed去计算可视区域的元素值:

computed: {
  visibleColumns({ columns }) {
    return columns.slice(
      Math.max(this.start, 0),
      Math.min(this.end, columns.length)
    )
  }
},

说明: 每一列宽度不一定是等宽的,这时候可以通过维护一个递减的值去获得最终start跟end的指针坐标(这里主要体会虚拟滚动的原理)如:

let startX = scrollLeft
let endX = scrollLeft + this.$el.clientWidth

const start = columns.findIndex((col) => {
  const colW = col.width || 100
  startX = startX - colW
  return startX < colW
})

const end = columns.findIndex((col) => {
  const colW = col.width || 100
  endX = endX - colW
  return endX < colW
})

更正:

上面有个bug,直接在table-container里面设置了width是不对的,而是要创建一个acho-table-phantom虚拟 div,把它的宽度设置成总宽度,用来撑起整个 div,生成滚动条。
滚动时,设置可视区域表格的transform位移属性为滚动时的偏移量,并更新可视列表数据。

.acho-table-phantom {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  z-index: -1;
}

虚拟滚动如何优化

  1. 节流:遇到scroll滚动的场景,节流能让事件的触发根据delay的事件而变得井然有序。
  2. requestAnimationFrame:scroll回调的处理可以使用requestAnimationFrame能最大程度保证画面不掉帧,因为setTimeout无法保证执行时机,而requestAnimationFrame在没帧渲染完后都会去执行其中的回调。
  1. transform:对于窗口的位移,大多数人都是使用padding-left处理,但这会不断产生重绘与回流,根据render tree渲染成图层到合成图层的过程,一旦同一个图层其中元素产生变更,就会重新去处理图层。因此可使用transform并指定csswill-change: transform;来告诉GUI进程这个用单独的图层去渲染。
  2. vue"就地更新"策略--用key属性渲染等......

拓展

table组件往往不单止显示数据那么简单,如果要求在表格进行搜索、排序、选择等功能时,就可以通过slot作用域具名插槽+动态组件来对table进行拓展,这样可以达到在dataSource数据整理时,可自定义每一列可选的功能。如

源码:

github.dev/AutumnWhj/v…

最后

虚拟列表在数据量级较大,dom节点渲染很多的情况下,不失为一种几乎完美的解决方案,用上之后,打开Chrome性能页面进行测试,会发现Rendering的时间大大大的减少,因此小伙伴们快用起来吧~~

以上为个人学习见解,一隅之见,欢迎交流学习!