优化实战 第 09 期 - 固定表头、固定列的高性能表格

2,194 阅读3分钟

粘性定位

  • 滚动容器

    具有 sticky 属性的元素,会固定在离它最近的一个拥有 滚动机制 的祖先元素上(overflow: auto)

    可以指定滚动容器的高度或最大高度来达到预期的效果

  • 效果呈现

    在一个滚动容器内

    当元素跨越特定阈值前,显示的效果与 position:relative 一致

    当元素跨越特定阈值后,显示效果与 position:fixed 一致

  • 注意事项

    父元素不能设置 overflow: hiddenoverflow: auto 属性

    必须指定 top, right, bottomleft 四个阈值其中之一,才可使粘性定位生效,否则其行为与相对定位相同

固定表头的表格

  • 模板代码

    <table class="table">
      <thead :class="{ 'table-thead-fixed': isFixedThead }">
        <tr>
          <th v-for="(column, index) of tableColumn" :key="index">
            {{ column.label }}
          <th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(row, rowIndex) of tableData" :key="rowIndex">
          <td
            v-for="(column, index) of tableColumn" :key="index"
            :class="{ [alignment[column.align || 'left']]: true }">
            <template v-if="column.type === 'number'">{{ rowIndex + 1 }}</template>
            <template v-else-if="column.prop">{{ row[column.prop] }}</template>
            <template v-else-if="column.slot">
              <slot :name="column.slot" :data="row"></slot>
            </template>
          </td>
        </tr>
      </tbody>
    </table>
    
  • 脚本代码

    export default {
      props: {
        tableColumn: {
          required: true,
          type: Array,
        },
        tableData: {
          required: true,
          type: Array,
        },
        isFixedThead: {
          default: true,
          type: Boolean,
        },
      },
      data () {
        return {
          alignment: Object.freeze({
            'left': 'text-left',
            'center': 'text-center',
            'right': 'text-right'
          })
        }
      }
    }
    
  • 风格代码

    .table {
      width: 100%;
      table-layout: fixed;
      border-collapse: collapse;
      border-spacing: 0;
      font-size: 14px;
      color: #333;
      border-top: 1px solid #ebeef5;
      & th {
        padding: 12px 15px;
        text-align: center;
        font-weight: 500;
        background-color: #F5F7FC;
      }
      & td {
        padding: 8px 15px;
      }
      & td {
        &.text-left {
          text-align: left;
        }
        &.text-center {
          text-align: center;
        }
        &.text-right {
          text-align: right;
        }
      }
      & th, & td {
        border: 1px solid #ebeef5;
      }
    }
    .table-thead-fixed {
      position: sticky;
      top: 0; z-index: 20;
    }
    

    表格初始化时,采用固定布局,各列宽度由第一行决定,后面指定的宽度会被忽略

    减少表格渲染过程中的重绘,提升性能

遇到的问题

  • 滚动时表格上方会缺少边框且还会有 1 像素的空隙

    .table-supplement-style {
      & .table-thead-fixed::before {
        position: absolute;
        top: -1px;
        content: '';
        width: 100%;
        height: 2px;
        background-color: #ebeef5;
      }
    }
    
    const { contains, add, remove } = this.tableEl.classList
    if (scrollTop >= offsetTop) {
      if (!contains('table-supplement-style')) {
        add('table-supplement-style')
      }
    } else {
      remove('table-supplement-style')
    }
    

    获取悬浮目标元素距离顶部的距离 offsetTop

    通过监听滚动容器的 scroll 事件,获取滚动条距离顶部的距离 scrollTop

    通过比对,来给表格添加补充样式解决问题

  • 表格的单元格设置了宽度,在悬浮效果下会失效,页面会有抖动感

    <table class="table">
      <colgroup>
        <col v-for="(column, index) of tableColumn" :key="index" :style="{ width: column.width ? column.width + 'px' : null }">
      </colgroup>
      ...
    </table>
    

    将表格的列宽通过 col 元素来设置,问题解决

固定列的表格(包括多列)

  • 模板代码

    <div class="scroll-container">
      <table class="table">
        <colgroup>
          <col v-for="(column, index) of tableColumn" :key="index" :style="{ width: column.width ? column.width + 'px' : null }">
        </colgroup>
        <thead>
          <tr>
            <th 
              v-for="(column, index) of tableColumn" :key="index" 
              :class="{ 'table-column-fixed': column.fixed }" :style="getStickyLeftValue(index)">
              {{ column.label }}
            </th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="(row, rowIndex) of tableData" :key="rowIndex">
            <td 
              v-for="(column, index) of tableColumn" :key="index" 
              :class="{ 'table-column-fixed': column.fixed, [alignment[column.align || 'left']]: true }"
              :style="getStickyLeftValue(index)">
              <template v-if="column.type === 'number'">
                {{ rowIndex + 1 }}
              </template>
              <template v-else-if="column.prop">
                {{ row[column.prop] }}
              </template>
              <template v-else-if="column.slot">
                <slot :name="column.slot" :data="row"></slot>
              </template>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
    
  • 脚本代码

    export default {
      computed: {
        stickyLeftValue() {
          const fixedColumns = this.tableColumn.filter(column => column.fixed)
          let m = new Map(), len = fixedColumns.length
          while(len--) {
            const computedWidth = fixedColumns.slice(0, len).reduce((sum, column) => (sum += column.width), 0)
            m.set(len, computedWidth)
          }
          return m
        }
      },
      methods: {
        getStickyLeftValue(index) {
          return { left: this.stickyLeftValue.get(index) + 'px' }
        }
      }
    }
    

    通过计算属性算出固定列距离左边的偏移量,在渲染的时候通过索引获取

  • 风格代码

    .scroll-container {
      overflow-x: auto;
    }
    .table-column-fixed {
      position: sticky;
      z-index: 20;
      background-color: #fff;
    }
    
  • 滚动时固定列的左右边框会消失

    .table {
      border-top: 1px solid #ebeef5;
      border-right: 1px solid #ebeef5;
      th, td {
        position: relative;
        &::before, &::after {
          content: '';
          position: absolute
        }
        &::before {
          left: 0; bottom: 0;
          width: 100%; height: 1px;
          background-color: #ebeef5;
        }
        &::after {
          left: 0; top: 0;
          height: 100%; width: 1px;
          background-color: #ebeef5;
        }
      }
    }
    

    使用伪元素模拟左边框和下边框,并且给表格元素添加上边框和右边框

  • 遇到的问题

    问题: 滚动时左上角的交叉单元格会被右边和下边的滚动元素覆盖

    解决: 设置层级 z-index 大于固定元素的层级即可解决

  • 一起交流学习

    加群交流看沸点

优化总结

基于原生的 CSS3 属性 position: sticky 实现的固定表头、固定列的表格,当数据达到一定量级的时候,在性能上会有很大的提升