Element UI table组件部分源码解读(table-body部分)

5,693 阅读3分钟

上一篇文章解读了Table组件的table-header部分,今天来介绍一下table-body的主体

再回顾一下table.vue的构成:

// 隐藏列
<div class="hidden-columns" ref="hiddenColumns"><slot></slot></div>
// 表头部分
<div class="el-table__header-wrapper"><table-header></table-header></div>
// 主体部分
<div class="el-table__body-wrapper"><table-body></table-body></div>
// 占位块,没有数据时渲染
<div class="el-table__empty-block"></div>
// 插入至表格最后一行之后的内容 slot插入
<div class="el-table__append-wrapper"><slot="append"></slot></div>
// 表尾部分
<div class="el-table__footer-wrapper"><table-foot></table-foot></div>
// 左侧固定列部分
<div class="el-table__fixed"></div>
// 右侧固定列部分
<div class="el-table__fixed-right"></div>
// 右侧固定列部分补丁(滚动条)
<div class="el-table__fixed-right-patch"></div>
// 用于列宽调整的代理
<div class="el-table__column-resize-proxy"></div>

table-body组件作为table的主体部分,实现上也比较复杂

Render

render是整个table-body的核心,先看一下模板:

render(h) {
    const data = this.data || [];
    return (
      <table
        class="el-table__body"
        cellspacing="0"
        cellpadding="0"
        border="0">
        <colgroup>
          {
            this.columns.map(column => <col name={ column.id } key={column.id} />)
          }
        </colgroup>
        <tbody>
          {
            data.reduce((acc, row) => {
              return acc.concat(this.wrappedRowRender(row, acc.length));
            }, [])
          }
          <el-tooltip effect={ this.table.tooltipEffect } placement="top" ref="tooltip" content={ this.tooltipContent }></el-tooltip>
        </tbody>
      </table>
    );
  },

可以看到,render内容比较少,比较常用的方式,一个<table>包裹<colgroup><tbody>组成。

可以看到关键的地方在这里this.wrappedRowRender(row, acc.length),这个wrappedRowRender是什么? 好像返回了很多东西,不急,我们在methods里面找一下:

wrappedRowRender方法

这里才是真正实现了三种row数据的渲染返回

wrappedRowRender(row, $index) {
    const store = this.store;
    const { isRowExpanded, assertRowKey } = store;
    const { treeData, lazyTreeNodeMap, childrenColumnName, rowKey } = store.states;
    if (this.hasExpandColumn && isRowExpanded(row)) {
        // 渲染扩展行
    } else if (Object.keys(treeData).length) {
        // 渲染树形列表
    } else {
        // 渲染普通行
        return this.rowRender(row, $index);
    }
}

关键的rowRender方法

先看一下最后的else,是最简单情况,直接调用rowRender返回,这里其他两种情况也要调用到:

rowRender(row, $index, treeRowData) {
    const { treeIndent, columns, firstDefaultColumnIndex } = this;
    const columnsHidden = columns.map((column, index) => this.isColumnHidden(index));
    const rowClasses = this.getRowClass(row, $index);
    let display = true;
    if (treeRowData) {
        rowClasses.push('el-table__row--level-' + treeRowData.level);
        display = treeRowData.display;
    }
    // 指令 v-show 会覆盖 row-style 中 display
    // 使用 :style 代替 v-show https://github.com/ElemeFE/element/issues/16995
    let displayStyle = display ? null : {
        display: 'none'
    };
    return (<tr
        style={ [displayStyle, this.getRowStyle(row, $index)] }
        class={ rowClasses }
        key={ this.getKeyOfRow(row, $index) }
        on-dblclick={ ($event) => this.handleDoubleClick($event, row) }
        on-click={ ($event) => this.handleClick($event, row) }
        on-contextmenu={ ($event) => this.handleContextMenu($event, row) }
        on-mouseenter={ _ => this.handleMouseEnter($index) }
        on-mouseleave={ this.handleMouseLeave }>
        {
          columns.map((column, cellIndex) => {
            const { rowspan, colspan } = this.getSpan(row, column, $index, cellIndex);
            if (!rowspan || !colspan) {
              return null;
            }
            const columnData = { ...column };
            columnData.realWidth = this.getColspanRealWidth(columns, colspan, cellIndex);
            const data = {
              store: this.store,
              _self: this.context || this.table.$vnode.context,
              column: columnData,
              row,
              $index
            };
            if (cellIndex === firstDefaultColumnIndex && treeRowData) {
              data.treeNode = {
                indent: treeRowData.level * treeIndent,
                level: treeRowData.level
              };
              if (typeof treeRowData.expanded === 'boolean') {
                data.treeNode.expanded = treeRowData.expanded;
                // 表明是懒加载
                if ('loading' in treeRowData) {
                  data.treeNode.loading = treeRowData.loading;
                }
                if ('noLazyChildren' in treeRowData) {
                  data.treeNode.noLazyChildren = treeRowData.noLazyChildren;
                }
              }
            }
            return (
              <td
                style={ this.getCellStyle($index, cellIndex, row, column) }
                class={ this.getCellClass($index, cellIndex, row, column) }
                rowspan={ rowspan }
                colspan={ colspan }
                on-mouseenter={ ($event) => this.handleCellMouseEnter($event, row) }
                on-mouseleave={ this.handleCellMouseLeave }>
                {
                  column.renderCell.call(
                    this._renderProxy,
                    this.$createElement,
                    data,
                    columnsHidden[cellIndex]
                  )
                }
              </td>
            );
          })
        }
      </tr>);
    }

代码行看起来比较多,但是只要理解本质就可以了:根据row和column数据,渲染出一个tr(即一行),里面很多属性获取,事件处理都进行了封装。

渲染扩展行

先判断一下是否有rowExpand(即配置了展开行功能),执行返回一个tr包裹的扩展行:

    const renderExpanded = this.table.renderExpanded;
    const tr = this.rowRender(row, $index);
    if (!renderExpanded) {
        console.error('[Element Error]renderExpanded is required.');
        return tr;
    }
    // 使用二维数组,避免修改 $index
    return [[
      tr,
      <tr key={'expanded-row__' + tr.key}>
        <td colspan={ this.columnsCount } class="el-table__expanded-cell">
          { renderExpanded(this.$createElement, { row, $index, store: this.store }) }
        </td>
      </tr>]];

这里也有两个关键的函数rowRender,和renderExpanded

renderExpanded就更简单了,因为扩展行是slot配置进去的,实际上是根据用户配置的slot直接render就可以。这个方法是在table-column中实现的

this.owner.renderExpanded = (h, data) => {
    return this.$scopedSlots.default
        ? this.$scopedSlots.default(data)
        : this.$slots.default;
};

渲染树形数据

再回到wrappedRowRender部分,看看else if分支做的事情, 这里判断了是否有treeData,即是否要渲染树形数据

    // 检查rowkey是否存在 在watcher.js实现
    assertRowKey();
    // TreeTable 时,rowKey 必须由用户设定,不使用 getKeyOfRow 计算
    // 在调用 rowRender 函数时,仍然会计算 rowKey,不太好的操作
    const key = getRowIdentity(row, rowKey);
    let cur = treeData[key];
    let treeRowData = null;
    if (cur) {
        treeRowData = {
            expanded: cur.expanded,
            level: cur.level,
            display: true
        };
        if (typeof cur.lazy === 'boolean') {
            if (typeof cur.loaded === 'boolean' && cur.loaded) {
              treeRowData.noLazyChildren = !(cur.children && cur.children.length);
            }
            treeRowData.loading = cur.loading;
        }
    }
    const tmp = [this.rowRender(row, $index, treeRowData)];
    // 渲染嵌套数据
    if (cur) {
        // currentRow 记录的是 index,所以还需主动增加 TreeTable 的 index
        let i = 0;
        const traverse = (children, parent) => {...}
        // 对于 root 节点,display 一定为 true
        cur.display = true;
        const nodes = lazyTreeNodeMap[key] || row[childrenColumnName];
        traverse(nodes, cur);
    }
    return tmp

渲染树型结构数据相对复杂,cur代表当前层的data数据,tmp暂存一下第一层的rowRender。 traverse是一个递归方法,遍历每一层子节点,处理后调用rowRender方法,添加到tmp中最后返回。 tmp最后实际上是一个从tree根节点遍历到子节点,所有tr行渲染结果的列表。

其他

除了最主要的render方法,其余的方法类似getCellClass, getRowClass,handleCellMouseEnter, handleMouseEnter之类工具方法都比较简单,这里不详细展开。