elementUI-table-表格性能优化-固定列导致的性能问题

3,044 阅读2分钟

elementUI的固定列实现

image.png

根据上图,我们得知Eui的固定列是通过多渲染一个table来实现的,然后设置该table宽度为所有固定列的宽度和,然后其他的列都visibility:hidden
左固定列会多一个table,右固定列会多一个table,也就是说我们传入的list就20条记录,但是渲染实际会是60条数据,性能差不用说了吧

Eui固定列实现的bug

IMG_20220830_103045(1).png 上图因为数据比较敏感,所以我做了模糊处理,主要功能是右侧有个固定列,然后table里某一列的高度不能超过三行,超过三行会出现省略号,所以我需要使用到-webkit-line-clamp: 3
但是这个属性使用时必须得visibility:visible,而固定列使用的table中的非固定列都是visibility:hidden(所以该行会超过3行),所以会导致主体table和固定列table的行高不一致,就导致上图的错位。

qqq.png 上图中,我设置了四个左固定列且开启了合计行,然后红框框处是无法拖动横向滚动条的,这是因为固定列的table是覆盖住了主体table的,而固定列的table的滚动条是禁止滚动的。

elementUI固定列缺点列举

  1. 会多渲染几个table,性能差
  2. 会导致固定列table和主体table的行高不一致,体现为错位
  3. 使用总计行时,会导致横向滚动条局部无滚动 这三个问题都没什么好办法解决,我试过。

自定义实现一个固定列

我使用了element-ui-2.15.5版本,其他版本会有些许偏差,可以加我微信,我远程帮你
不就是固定列嘛,position:sticky就能实现,大家可以去了解一下这个属性,地址
position:sticky的浏览器兼容:

`O$[)DVC81S48]T9Q}5QPQW(1).png

改造过程

给eui的组件二次改造的方法有三种,我的另一篇文章写了教程。
而下文改造的方式是将element-ui项目拉下来,然后本地改造,然后运行看效果的。
如果有人希望能将table组件迁移到业务代码中二次改造的话,可以加我微信,我帮你远程。

体验地址

录制_2022_09_02_15_48_47_370.gif 项目地址

*** 重要 ***

对应的文件修改可以参考该commit,以下所有文件修改都可以在该commit找到对应,只不过多了注释。

  1. 打开packages/table/src/store/watcher.js,在data中添加两个属性
...
fixedColumns: [],

/* 新增 */
// 新增左固定列数组
fixedLeftColumns: [],
// 新增右固定列数组
fixedRightColumns: [],
/**/

rightFixedColumns: [],
...

updateColumns方法中添加两行代码

updateColumns() {
  ...
  states.fixedColumns = _columns.filter((column) => column.fixed === true || column.fixed === 'left');
  
  /* 新增 */
  // 在store中记录左固定和右固定的的列
  states.fixedLeftColumns = _columns.filter((column) => column.fixedLeft === true);
  states.fixedRightColumns = _columns.filter((column) => column.fixedRight === true);
  
  states.rightFixedColumns = _columns.filter((column) => column.fixed === 'right');
  ...
 }
  1. 打开packages/table/src/table-body.js,原getCellStyle方法:决定表格某个单元的样式是什么。我们直接改造该方法
getCellStyle(rowIndex, columnIndex, row, column) {
  const cellStyle = this.table.cellStyle;
  let rsl = {};
  if (typeof cellStyle === 'function') {
    rsl = cellStyle.call(null, {
      rowIndex,
      columnIndex,
      row,
      column
    }) || {};
  } else {
    rsl = cellStyle || {};
  }
  // 如果该列是左固定列或者右固定列
  if (column.fixedLeft || column.fixedRight) {
    let obj = {};
    // 构造一个position:sticky的样式对象
    obj.position = 'sticky';
    obj.zIndex = 1;
    obj.backgroundColor = 'inherit';
    // 比如第一个固定列的left是0,第二个左固定列的left得是第一个固定列的宽度,所以得求和。
    column.fixedLeft && (obj.left = column.stickyLeft + 'px');
    column.fixedRight && (obj.right = column.stickyRight + 'px');
    // 根据开发者返回的样式是对象还是字符串还是数组来决定如何将sticky样式合并进去
    switch (Object.prototype.toString.call(rsl)) {
      case '[object Object]':
        rsl = Object.assign(rsl, obj);
        break;
      case '[object Array]':
        rsl.push(obj);
        break;
      case '[object String]':
        let tempStr = JSON.stringify(obj);
        tempStr = tempStr.slice(1, tempStr.length - 2).replace(/\,"/g, ';"').replace(/"/, '');
        tempStr[tempStr.length - 1] !== ';' && (tempStr += ';');
        rsl = tempStr + rsl;
        break;
    }
  }
  return rsl;
},
  1. 打开packages/table/src/table-column.js,该文件是列的配置,原props是
props: {
    ...
    fixed: [Boolean, String],
    ...
}

我们添加两行,代表该列可以定义为新形式的左固定列,或者右固定列

props: {
    ...
    fixed: [Boolean, String],
    // 左固定列配置
    fixedLeft: Boolean,
    // 有固定列配置
    fixedRight: Boolean,
    ...
}

在原created钩子函数中

created() {
    ...
    const basicProps = ['columnKey', 'label', 'className', 'labelClassName', 'type', 'renderHeader', 'formatter', 'fixed', 'resizable'];
    ...
 }

给basicProps添加四个item

created() {
    ...
    const basicProps = ['columnKey', 'label', 'className', 'labelClassName', 'type', 'renderHeader', 'formatter', 'fixed', 'resizable', 
    'fixedLeft', 'fixedRight','stickyLeft', 'stickyRight'];
    ...
}
  1. 打开packages/table/src/table-footer.js,该文件是table的底部,比如合计行。原render函数的return
render(h) {
    return (
        ...
        <tbody class={ [{ 'has-gutter': this.hasGutter }] }>
          <tr>
            {
              this.columns.map((column, cellIndex) => 
              <td
                key={cellIndex}
                colspan={ column.colSpan }
                rowspan={ column.rowSpan }
                class={ this.getRowClasses(column, cellIndex) }>
                <div class={ ['cell', column.labelClassName] }>
                  {
                    sums[cellIndex]
                  }
                </div>
              </td>)
            }
          ...
       )
}

我们添加一行style={ this.getFooterCellStyle(column) }决定该列的样式

render(h) {
    return (
        ...
        <tbody class={ [{ 'has-gutter': this.hasGutter }] }>
          <tr>
            {
              this.columns.map((column, cellIndex) => 
              <td
                key={cellIndex}
                colspan={ column.colSpan }
                rowspan={ column.rowSpan }
                style={ this.getFooterCellStyle(column) }
                class={ this.getRowClasses(column, cellIndex) }>
                <div class={ ['cell', column.labelClassName] }>
                  {
                    sums[cellIndex]
                  }
                </div>
              </td>)
            }
         ...
       )
 }

然后我们在methods中添加getFooterCellStyle方法

getFooterCellStyle(column) {
  let rsl = {};
  // 如果该列是左固定列或者右固定列
  if (column.fixedLeft || column.fixedRight) {
    let obj = {};
    obj.position = 'sticky';
    obj.backgroundColor = 'inherit';
    obj.zIndex = 1;
    // 比如第一个固定列的left是0,第二个左固定列的left得是第一个固定列的宽度,所以得求和。
    column.fixedLeft && (obj.left = column.stickyLeft + 'px');
    column.fixedRight && (obj.right = column.stickyRight + (this.tableLayout.scrollY ? this.tableLayout.gutterWidth : 0) + 'px');
    rsl = Object.assign(rsl, obj);
  }
  return rsl;
}
  1. 打开packages/table/src/table-header.js,该文件是表头的配置,原getHeaderCellStyle方法:决定某个单元格的样式是什么。我们直接将其改造
getHeaderCellStyle(rowIndex, columnIndex, row, column) {
  const headerCellStyle = this.table.headerCellStyle;
  let rsl = {};
  if (typeof headerCellStyle === 'function') {
    rsl = headerCellStyle.call(null, {
      rowIndex,
      columnIndex,
      row,
      column
    }) || {};
  } else {
    rsl = headerCellStyle || {};
  }
  // 如果该列是左固定列或者右固定列
  if (column.fixedLeft || column.fixedRight) {
    let obj = {};
    obj.position = 'sticky';
    obj.zIndex = 1;
    // 比如第一个固定列的left是0,第二个左固定列的left得是第一个固定列的宽度,所以得求和。
    column.fixedLeft && (obj.left = column.stickyLeft + 'px');
    column.fixedRight && (obj.right = column.stickyRight + (this.tableLayout.scrollY ? this.tableLayout.gutterWidth : 0) + 'px');
    // 根据开发者返回的样式是对象还是字符串还是数组来决定如何将sticky样式合并进去
    switch (Object.prototype.toString.call(rsl)) {
      case '[object Object]':
        rsl = Object.assign(rsl, obj);
        break;
      case '[object Array]':
        rsl.push(obj);
        break;
      case '[object String]':
        let tempStr = JSON.stringify(obj);
        tempStr = tempStr.slice(1, tempStr.length - 2).replace(/\,"/g, ';"').replace(/"/, '');
        tempStr[tempStr.length - 1] !== ';' && (tempStr += ';');
        rsl = tempStr + rsl;
        break;
    }
  }
  return rsl;
},
  1. 打开packages/table/src/table-layout.js,该文件是table布局相关的属性。原constructor函数如下
constructor(options) {
    ...
    this.fixedWidth = null;
    this.rightFixedWidth = null;
    ...
}

我们添加两个属性

constructor(options) {
    ...
    this.fixedWidth = null;
    this.rightFixedWidth = null;
    /* 新增 */
    // 左右固定列的总宽度
    this.fixedLeftWidth = null;
    this.fixedRightWidth = null;
    ...
}

updateColumnsWidth方法:更新列的宽度

updateColumnsWidth() {
    ...
    const fixedColumns = this.store.states.fixedColumns;

    if (fixedColumns.length > 0) {
      let fixedWidth = 0;
      fixedColumns.forEach(function(column) {
        fixedWidth += column.realWidth || column.width;
      });

      this.fixedWidth = fixedWidth;
    }
    ...
}

添加两个属性、两个逻辑

updateColumnsWidth() {
    ...
    const fixedColumns = this.store.states.fixedColumns;

    if (fixedColumns.length > 0) {
      let fixedWidth = 0;
      fixedColumns.forEach(function(column) {
        fixedWidth += column.realWidth || column.width;
      });

      this.fixedWidth = fixedWidth;
    }
    
    /* 新增 */
    const fixedLeftColumns = this.store.states.fixedLeftColumns;
    // 如果存在左侧固定列
    if (fixedLeftColumns.length > 0) {
      let sum = 0;
      let fixedLeftWidth = 0;
      fixedLeftColumns.forEach(column => {
        // 更新每一个固定列的left属性,比如第一个固定列的left是0,第二个左固定列的left得是第一个固定列的宽度,所以得求和。
        column.stickyLeft = sum;
        sum += column.realWidth || column.width;
        fixedLeftWidth += column.realWidth || column.width;
      });
      // 更新左固定列的总宽度
      this.fixedLeftWidth = fixedLeftWidth;
    }
    
    const fixedRightColumns = this.store.states.fixedRightColumns;
    
    if (fixedRightColumns.length > 0) {
      let sum = 0;
      let fixedRightWidth = 0;
      fixedRightColumns.forEach(column => {
        // 更新每一个固定列的right属性,比如第一个固定列的right是0,第二个左固定列的right得是第一个固定列的宽度,所以得求和。
        column.stickyRight = sum;
        sum += column.realWidth || column.width;
        fixedRightWidth += column.realWidth || column.width;
      });
      // 更新右固定列的总宽度
      this.fixedRightWidth = fixedRightWidth;

      this.notifyObservers('columns');
    }
    ...
}
  1. 打开packages/table/src/table.vue,该文件是table主体。在原template中,
<template>
    ...
    <div
      v-if="showSummary"
      v-show="data && data.length > 0"
      v-mousewheel="handleHeaderFooterMousewheel"
      class="el-table__footer-wrapper"
      ref="footerWrapper">
      <table-footer
        :store="store"
        :border="border"
        :sum-text="sumText || t('el.table.sumText')"
        :summary-method="summaryMethod"
        :default-sort="defaultSort"
        :style="{
          width: layout.bodyWidth ? layout.bodyWidth + 'px' : ''
        }">
      </table-footer>
    </div>
    ...
</template>

添加4个div

<template>
    ...
    <div
      v-if="showSummary"
      v-show="data && data.length > 0"
      v-mousewheel="handleHeaderFooterMousewheel"
      class="el-table__footer-wrapper"
      ref="footerWrapper">
      <table-footer
        :store="store"
        :border="border"
        :sum-text="sumText || t('el.table.sumText')"
        :summary-method="summaryMethod"
        :default-sort="defaultSort"
        :style="{
          width: layout.bodyWidth ? layout.bodyWidth + 'px' : ''
        }">
      </table-footer>
    </div>
    
    <!-- 新增 -->
    <!-- 用来模拟阴影的 -->
    <div 
      v-if="haveFixedLeft && scrollPosition !== 'left'" 
      :style="[{'position':'absolute','top':'0px',
                'left':layout.fixedLeftWidth - columns.filter(item=>item.fixedLeft).length + 'px',
                'z-index':2,'width':'1px',
                'box-shadow':'2px 0px 3px 0px rgba(0,0,0,0.75)'},
              fixedHeight]">
    </div>
    <div 
      v-if="haveFixedLeft && scrollPosition !== 'left'" 
      :style="[{'position':'absolute','left':'0px',
                'width':layout.fixedLeftWidth - columns.filter(item=>item.fixedLeft).length + 'px',
                'height':'1px',
                'box-shadow':'2px 0px 3px 0px rgba(0,0,0,0.75)'},
              fixedHeightForShadow]">
    </div>
    <div 
      v-if="haveFixedRight && scrollPosition !== 'right'" 
      :style="[{'position':'absolute','top':'0px',
                'right':layout.fixedRightWidth - columns.filter(item=>item.fixedRight).length + 'px',
                'z-index':2,'width':'1px',
                'box-shadow':'-2px 0px 3px 0px rgba(0,0,0,0.75)'},
              fixedHeight]">
    </div>
    <div 
      v-if="haveFixedRight && scrollPosition !== 'left'" 
      :style="[{'position':'absolute','right':'0px',
                'width':layout.fixedRightWidth - columns.filter(item=>item.fixedRight).length + 'px',
                'height':'1px',
                'box-shadow':'2px 0px 3px 0px rgba(0,0,0,0.75)'},
              fixedHeightForShadow]">
    </div>
    ...
</template>

在template的另一处

<div
  v-if="rightFixedColumns.length > 0"
  class="el-table__fixed-right-patch"
  ref="rightFixedPatch"
  :style="{
    width: layout.scrollY ? layout.gutterWidth + 'px' : '0',
    height: layout.headerHeight + 'px'
  }"></div>

添加一个条件haveFixedRight

<div
  v-if="rightFixedColumns.length > 0 || haveFixedRight"
  class="el-table__fixed-right-patch"
  ref="rightFixedPatch"
  :style="{
    width: layout.scrollY ? layout.gutterWidth + 'px' : '0',
    height: layout.headerHeight + 'px'
  }"></div>

在computed添加两个属性

computed:{
    // 是否有新型左侧固定列
    haveFixedLeft() {
    return this.columns.filter(item => item.fixedLeft).length;
    },

    // 是否有新型右侧固定列
    haveFixedRight() {
    return this.columns.filter(item => item.fixedRight).length;
    },
    ...
}

改造shouldUpdateHeight方法为

shouldUpdateHeight() {
    return this.height ||
      this.maxHeight ||
      this.fixedColumns.length > 0 ||
      this.rightFixedColumns.length > 0 ||
      this.fixedLeftColumns.length > 0 ||
      this.fixedRightColumns.length > 0;
},

在methods里添加一个fixedHeightForShadow方法

fixedHeightForShadow() {
    if (this.maxHeight) {
      if (this.showSummary) {
        return {
          bottom: 0
        };
      }
      return {
        bottom: (this.layout.scrollX && this.data.length) ? this.layout.gutterWidth + 'px' : ''
      };
    } else {
      if (this.showSummary) {
        return {
          top: this.layout.tableHeight ? this.layout.tableHeight + 'px' : ''
        };
      }
      return {
        top: this.layout.viewportHeight ? this.layout.viewportHeight + 'px' : ''
      };
    }
},

改造...mapStates:

...mapStates({
    selection: 'selection',
    columns: 'columns',
    tableData: 'data',
    fixedColumns: 'fixedColumns',
    rightFixedColumns: 'rightFixedColumns',
    /* 新增 */
    // 从存储中心获取左固定列和右固定列
    fixedLeftColumns: 'fixedLeftColumns',
    fixedRightColumns: 'fixedRightColumns',
})
  1. 打开packages/theme-chalk/src/table.scss文件,找到@include m(striped),然后改造为
@include m(striped) {
    & .el-table__body {
      & tr td {
        background-color: inherit;
      }
      & tr.el-table__row--striped {
        td {
          background: #FAFAFA;
        }

        &.current-row td {
          background-color: $--table-current-row-background-color;
        }
      }
    }
  }

看到这里的看官,麻烦点个赞赞

有问题,可以加我微信:17688172759,我帮你远程