上一篇文章解读了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之类工具方法都比较简单,这里不详细展开。