原生表格的虚拟滚动实践(上)

内容列表

前言

在前端开发中,表格一直都是最复杂的组件之一,由于公司业务,需要在低版本浏览器中使用,并且需要实现固定列,所以市面上使用(使用position:sticky 实现粘性布局)的方案就被pass掉了,因为该特性只能在浏览器56版本以上才能使用,需要使用原生表格实现虚拟滚动,本篇仅分享表格竖向的虚拟滚动。

原生表格的虚拟滚动

原生表格

原生表格的虚拟滚动,主要是指使用原生表格标签,通过计算表格的scrollTop来控制表格的显示区域,从而实现表格的虚拟滚动。

使用

// 这个表格使用核心就是传入两个数组,一个是列数组,一个是原始数据
<BaseTable
  :dataSource="dataSource"
  :columns="columns"
/>

原生表格(竖向)虚拟滚动实现

核心是使用padding的top与bottom来控制表格的显示区域,通过监听表格的scroll事件,来计算表格的scrollTop,从而控制表格的显示区域。流程如下:

核心需要根据当前下述状态中startIndex与endIndex乘以trHeight的去计算顶部padding与底部padding的高度,但是这种会导致渲染上存在一些渲染闪烁的问题,底层原因就是浏览器性能,在mac或者高性能电脑上就不是很明显

this.virtualState = {
  // trHeight 一个行元素高度
  trHeight: 20,
  // 当前渲染的数据量
  containSize: 0,
  // 当前需要展示的表格dom高度
  tableDomHeight: 0,
  // 虚拟滚动当前列表的开始下标
  startIndex: 0,
  // 虚拟滚动当前列表的结束下标
  endIndex: 0,
  // 顶部填充
  topFill: 0,
  //底部填充
  bottomFill: 0,
  // 滚动距离顶部的距离
  scrollTop: 0,
};
<!-- 在上面拿到的virtualState.topFill与virtualState.bottomFill就可以填充到这里实现表格高度的变化 -->
<template>
  <table
    ref="contentDiv"
    class="table-content"
    :cellspacing="0"
    :class="tableStyle"
    :cellpadding="0"
    :border="0"
    :style="[
      { paddingTop: virtualState.topFill + 'px' },
      { paddingBottom: virtualState.bottomFill + 'px' },
    ]"
    @mousedown="mouseSelectDown($event)"
    @mouseup="mouseSelectUp"
    @dblclick="handleRowDoubleClick($event)"
  >
  <!--这里是为了兼容低版本浏览器49表格所需要的配置-->
    <caption style="padding: 0px"></caption>
    <colgroup>
      <col
        v-for="(columnsItemCol, index) in props.columnsItem"
        :key="index"
        :name="`my_column_index_${index}`"
        :width="columnsItemCol.width"
        :style="{ width: `${columnsItemCol.width}px` }"
      />
    </colgroup>
    <thead>
      <tr
        v-for="(dataItem, rowIndex) in virtualData"
        :key="rowIndex"
        :data-rowIndex="rowIndex"
        :data-index="dataItem.index.value"
        @click.right.prevent="removeHandler(dataItem, $event)"
      >
        <td
          v-for="columnsItemTh in props.columnsItem"
          :colspan="1"
          :rowspan="1"
          :data-index="dataItem.index.value"
          :data-rowIndex="rowIndex"
        >
          <!-- 无论是否使用插槽都需要使用阴影 -->
          <div class="item">
            <div
              v-show="selection.includes(dataItem.ONLY_ONE_KEY)"
              :data-rowIndex="rowIndex"
              :data-index="dataItem.index.value"
              class="highlighted"
            />
            <slot
              name="content"
              :column-config="columnsItemTh"
              :column-value="dataItem"
              :index="dataItem.index.value"
              :row-index="rowIndex"
            >
              <div
                class="cell"
                :data-rowIndex="rowIndex"
                :data-index="dataItem.index.value"
                :style="getTextAlign(columnsItemTh)"
              >
                <p
                  class="text"
                  :data-rowIndex="rowIndex"
                  :data-index="dataItem.index.value"
                  :style="[getStyle(dataItem, columnsItemTh)]"
                >
                  {{ getTdCellResult(dataItem, columnsItemTh) }}
                </p>
              </div>
            </slot>
          </div>
        </td>
      </tr>
    </thead>
  </table>
</template>

优化方案1:

但是实际上这样会导致表格的滚动不流畅,因为每次滚动都会重新计算表格的scrollTop,所以需要使用requestAnimationFrame来优化,每次滚动都会将当前的scrollTop保存起来,然后在requestAnimationFrame中计算表格的scrollTop,从而实现表格的平滑滚动。

优化方案2-参考react-virtualized这个组件使用:

缓存表格的预估行高:

平滑滚动的要点 平滑性也是虚拟滚动的要点之一,滚动过程中应当避免因 blank 或行高变化而产生的抖动。滚动分为两种:

  • 缓慢滚动:上一次渲染的行 与 下一次渲染的行有交集
  • 快速滚动:上一次渲染的行 与 下一次渲染的行没有交集,# 即两次渲染之间部分行被跳过了

为了实现稳定高效且平滑的虚拟滚动,组件要注意以下几点:

  • 缓慢滚动时,一些行会出现或消失,topBlank/bottomBlank 要同步地发生变化
  • 例如向下滚动时,表格底部出现新的一行,行高为 50px,那么此时 bottomBlank 要同步地减小 50px
  • 快速滚动时,要在短时间内给出一个大致的渲染范围;范围不需要很准确,要计算过程要快
  • 渲染范围的计算结果要保持稳定:
  • 相同的输入产生相同的输出
  • 较大的 offset 会产生较大的 startIndex/endIndex,较大的 maxRenderHeight 会渲染更多的行
  • 滚动到底部时,bottomBlank 的计算结果要等于 0,endIndex 要等于 dataSource.length

参考

  1. https://www.yuque.com/shinima/blog/nbgglx
  2. https://www.yuque.com/shinima/blog/llo9ro

相关

浏览器对象模型(BOM)

2018-05-15

在网页开发中,我们通常专注于内容的设计,而有些时候我们需要进行不同窗口之间的交互,这时候我们就需要学习如何运用 BOM 中的许多核心对象,及其属性、方法。

了解更多

Web 前端调试工具:SourceMap 文件

2021-11-28

Web 前端项目在生产环境发布的代码是经过混淆和压缩的,如何调试则成为了一个难题,SourceMap 文件则是一个解决该问题时可以利用的很好的工具。

了解更多

桥接模式:跨平台的事件机制设计

2022-06-12

最近在做图表组件库的技术调研的架构方案设计,参考了很多开源库的源码,发现其中跨平台的事件机制设计很值得学习,如果要用软件设计模式来解释,那大概就是桥接模式了。

了解更多