上一篇文章解读了Table组件的store部分,今天来介绍一下主体的构成
回顾一下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-header
table-header较为复杂,是表格中的核心组成部分: 先看一下模板,这里使用的是jsx语法,提供了一个render函数:
render(h) {
const originColumns = this.store.states.originColumns;
const columnRows = convertToRows(originColumns, this.columns);
// 是否拥有多级表头
const isGroup = columnRows.length > 1;
if (isGroup) this.$parent.isGroup = true;
return (...)
首先通过store获取到columns ,这里originColumns是什么?查找store源码不难发现:
originColumns = fixedColumns + notFixedColumns + rightFixedColumns
其实就是左侧固定列+普通列+右侧固定列的集合,因为这里可能有嵌套等复杂情况的表头,需要调用convertToRows将所有的列信息转换成表头的行信息,包含层级,colspan等详细信息,根据转换后的columnRows判断是否存在多级表头。
下面是渲染的部分,简化后如下:
<table>
<colgroup></colgroup>
<thead>
<tr>
<th>
<div>...</div>
</th>
<tr>
</thead>
</table>
可以看到,实际上table-head也是一个独立的table
<colgroup>
{
this.columns.map(column => <col name={ column.id } key={column.id} />)
}
{
this.hasGutter ? <col name="gutter" /> : ''
}
</colgroup>
colgroup通过column遍历返回一个col的列表,最后判断一下hasGutter,即不存在固定列的时候,需要多加一列用于调整宽度,否则会造成表头和表体错位
hasGutter() {
return !this.fixed && this.tableLayout.gutterWidth;
}
往下看thead部分,信息量有点大:
<thead class={ [{ 'is-group': isGroup, 'has-gutter': this.hasGutter }] }>
{
this._l(columnRows, (columns, rowIndex) =>
<tr
style={ this.getHeaderRowStyle(rowIndex) }
class={ this.getHeaderRowClass(rowIndex) }
>
{
columns.map((column, cellIndex) = (<th>...</th>)
}
{
this.hasGutter ? <th class="gutter"></th> : ''
}
</tr>
)
}
</thead>
isGroup在上面说了,是否多级表头。这里有个神奇的this._l方法,找了好久,最后在vue源码找到,这是vue内置的一个renderList方法的别名,
其实就是遍历,可以理解为 v-for 或者 Array.map。
getHeaderRowStyle 和 getHeaderRowClass是暴露出来的两个props,允许用户自定义样式和class。
下面看下column的渲染部分,比较多,直接看源码
columns.map((column, cellIndex) => (<th
// 提前计算好的colSpan和rowSpan
colspan={ column.colSpan }
rowspan={ column.rowSpan }
// 处理鼠标事件
on-mousemove={ ($event) => this.handleMouseMove($event, column) }
on-mouseout={ this.handleMouseOut }
on-mousedown={ ($event) => this.handleMouseDown($event, column) }
on-click={ ($event) => this.handleHeaderClick($event, column) }
on-contextmenu={ ($event) => this.handleHeaderContextMenu($event, column) }
// 处理th的样式
style={ this.getHeaderCellStyle(rowIndex, cellIndex, columns, column) }
class={ this.getHeaderCellClass(rowIndex, cellIndex, columns, column) }
key={ column.id }>
<div class={ ['cell', column.filteredValue && column.filteredValue.length > 0 ? 'highlight' : '', column.labelClassName] }>
{
column.renderHeader
? column.renderHeader.call(this._renderProxy, h, { column, $index: cellIndex, store: this.store, _self: this.$parent.$vnode.context })
: column.label
}
{
// 是否允许排序,如果允许出现排序图标和绑定事件
column.sortable ? (<span
class="caret-wrapper"
on-click={ ($event) => this.handleSortClick($event, column) }>
<i class="sort-caret ascending"
on-click={ ($event) => this.handleSortClick($event, column, 'ascending') }>
</i>
<i class="sort-caret descending"
on-click={ ($event) => this.handleSortClick($event, column, 'descending') }>
</i>
</span>) : ''
}
{
// 是否允许过滤,如果允许出现过滤图标和绑定事件
column.filterable ? (<span
class="el-table__column-filter-trigger"
on-click={ ($event) => this.handleFilterClick($event, column) }>
<i class={ ['el-icon-arrow-down', column.filterOpened ? 'el-icon-arrow-up' : ''] }></i>
</span>) : ''
}
</div>
</th>)
}
需要注意的是这里:
column.renderHeader ? column.renderHeader.call(this._renderProxy, h, { column, $index: cellIndex, store: this.store, _self: this.$parent.$vnode.context }): column.label
renderHeader实际上是一个渲染函数,为外部传入,通过该渲染函数实现自定义表头的渲染。如果没有就直接显示label
到这里模板部分分析结束,看下其他属性:
computed: {
// 返回el-table实例
table() {
return this.$parent;
},
// 上面分析过,判断是否存在滚动条,存在需要进行宽度补丁
hasGutter() {
return !this.fixed && this.tableLayout.gutterWidth;
},
// 一些常用的全局变量
...mapStates({
columns: 'columns',
isAllSelected: 'isAllSelected',
leftFixedLeafCount: 'fixedLeafColumnsLength',
rightFixedLeafCount: 'rightFixedLeafColumnsLength',
columnsCount: states => states.columns.length,
leftFixedCount: states => states.fixedColumns.length,
rightFixedCount: states => states.rightFixedColumns.length
})
},
methods: {
// 判断当前单元格是否需要隐藏,根据colspan和固定的列数量计算
isCellHidden(index, columns) {}
// 调用外部的同名方法
getHeaderRowStyle(rowIndex) {}
// 调用外部的同名方法
getHeaderRowClass(rowIndex) {}
// 调用外部的同名方法
getHeaderCellStyle(rowIndex, columnIndex, row, column) {}
// 根据是否隐藏,排序等,计算出一个class列表
getHeaderCellClass(rowIndex, columnIndex, row, column) {}
// 切换全选
toggleAllSelection(event) {}
// 处理过滤器的点击事件,出现过滤器面板
handleFilterClick(event, column) {}
// 处理表头的点击事件,判断是sort还是filter
handleHeaderClick(event, column) {}
// 处理contextmenu(鼠标右键)事件,直接抛出给用户
handleHeaderContextMenu(event, column) {}
// 鼠标按下事件,内部主要有拖动宽度相关逻辑的处理
handleMouseDown(event, column) {}
// mousemove处理器,处理拖动逻辑
handleMouseMove(event, column) {}
// mouseout 事件,主要是失去焦点
handleMouseOut() {}
}
handleMouseDown实现了表头的拖动,这里详细分析下:
handleMouseDown(event, column) {
// 服务端渲染直接返回
if (this.$isServer) return;
// 如果是嵌套形式的表头,也不能拖动(只允许拖动子级)
if (column.children && column.children.length > 0) return;
/* istanbul ignore if */
// 如果允许拖动宽度并且存在border的话
if (this.draggingColumn && this.border) {
// 拖动标志位
this.dragging = true;
// 用来控制table上的resizeProxy是否显示
this.$parent.resizeProxyVisible = true;
// 计算table Vue实例,dom实例,左侧偏移
const table = this.$parent;
const tableEl = table.$el;
const tableLeft = tableEl.getBoundingClientRect().left;
// 寻找当前拖动的元素
const columnEl = this.$el.querySelector(`th.${column.id}`);
const columnRect = columnEl.getBoundingClientRect();
// 剩余最小宽度的计算,最少需要预留30px
const minLeft = columnRect.left - tableLeft + 30;
addClass(columnEl, 'noclick');
//定义一个临时状态
this.dragState = {
// 起始x坐标
startMouseLeft: event.clientX,
// 起始元素右侧剩余量
startLeft: columnRect.right - tableLeft,
// 起始元素左侧剩余量
startColumnLeft: columnRect.left - tableLeft,
tableLeft
};
// resizeProxy为一个拖动的元素的显示,动态改变其位置
const resizeProxy = table.$refs.resizeProxy;
resizeProxy.style.left = this.dragState.startLeft + 'px';
document.onselectstart = function() { return false; };
document.ondragstart = function() { return false; };
const handleMouseMove = (event) => {
// deltaLeft拖动的偏移量为鼠标当前位置 - 起始位置
const deltaLeft = event.clientX - this.dragState.startMouseLeft;
// 需要移动的距离
const proxyLeft = this.dragState.startLeft + deltaLeft;
// 改变resizeProxy的位置
resizeProxy.style.left = Math.max(minLeft, proxyLeft) + 'px';
};
const handleMouseUp = () => {
if (this.dragging) {
// 获取起始状态
const {
startColumnLeft,
startLeft
} = this.dragState;
// 计算最终鼠标位置,转换为整型
const finalLeft = parseInt(resizeProxy.style.left, 10);
// 最终拖动的宽度
const columnWidth = finalLeft - startColumnLeft;
// 改变column的宽度
column.width = column.realWidth = columnWidth;
table.$emit('header-dragend', column.width, startLeft - startColumnLeft, column, event);
// ***触发一下页面重新布局更新dom*** 【非常重要】,这里完成了对表体的重新布局
this.store.scheduleLayout();
document.body.style.cursor = '';
this.dragging = false;
this.draggingColumn = null;
this.dragState = {};
table.resizeProxyVisible = false;
}
// 移除事件绑定
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.onselectstart = null;
document.ondragstart = null;
setTimeout(function() {
removeClass(columnEl, 'noclick');
}, 0);
};
// 绑定事件
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
},