表格组件
表格结构
表头的实现
表头的外层为table 内部包含 hColgroup 和 table-header 组件
<template>
<!--header-wrapper 的表头 tableLayout === 'fixed'" 渲染-->
<div v-if="showHeader && tableLayout === 'fixed'">
<table>
<hColgroup />
<table-header />
</table>
</div>
<!--body-wrapper 的表头 tableLayout === 'auto'" 渲染-->
<div>
<table>
<hColgroup />
<table-header />
<table-body/>
</table>
</div>
</template>
hColgroup 组件
hColgroup 属性
| 属性 | 说明 | 类型 | 可选值 | 默认值 |
|---|---|---|---|---|
| tableLayout | 定义了表格行和列的算法table-layout | string | auto/fixed | fixed |
| columns | 每一列的样式 | array | [] |
hColgroup 组件生成列 colgroup 以达到对表格列的控制(colgroup)。
table-header 组件
table-header 属性
| 属性 | 说明 | 类型 | 可选值 | 默认值 |
|---|---|---|---|---|
| fixed | string | '' | ||
| store | table内部的状态管理,类似vuex | Object | ||
| border | 是否设置边框线 | Boolean | false | |
| defaultSort | table 默认排序 | Object | {prop: ''; order: ''} |
table-header 的render函数
columnRows 是通过 props.store.state 取出的,是一个数组可能包含多个 tr 用于多级表头。
每个 tr 再 渲染 th 并绑定事件。
th 下第一个 div 为当前的 表头的主要内容。
判断是否含有 表头渲染函数 renderHeader 有的话使用其返回值,没有的话使用 label 作为表头内容。
判断是否有排序(sort),有的话 渲染 升序和降序按钮。
判断是否 使用过滤 (filter), 则渲染 FilterPanel 组件。
render() {
return h(
'thead',
{
class: { [ns.is('group')]: isGroup },
},
columnRows.map((subColumns, rowIndex) =>
h(
'tr',
{
class: getHeaderRowClass(rowIndex),
key: rowIndex,
style: getHeaderRowStyle(rowIndex),
},
subColumns.map((column, cellIndex) => {
if (column.rowSpan > rowSpan) {
rowSpan = column.rowSpan
}
return h(
'th',
{
class: getHeaderCellClass(
rowIndex,
cellIndex,
subColumns,
column
),
colspan: column.colSpan,
key: `${column.id}-thead`,
rowspan: column.rowSpan,
style: getHeaderCellStyle(
rowIndex,
cellIndex,
subColumns,
column
),
onClick: ($event) => handleHeaderClick($event, column),
onContextmenu: ($event) =>
handleHeaderContextMenu($event, column),
onMousedown: ($event) => handleMouseDown($event, column),
onMousemove: ($event) => handleMouseMove($event, column),
onMouseout: handleMouseOut,
},
[
h(
'div',
{
class: [
'cell',
column.filteredValue && column.filteredValue.length > 0
? 'highlight'
: '',
column.labelClassName,
],
},
[
column.renderHeader
? column.renderHeader({
column,
$index: cellIndex,
store,
_self: $parent,
})
: column.label,
column.sortable &&
h(
'span',
{
onClick: ($event) => handleSortClick($event, column),
class: 'caret-wrapper',
},
[
h('i', {
onClick: ($event) =>
handleSortClick($event, column, 'ascending'),
class: 'sort-caret ascending',
}),
h('i', {
onClick: ($event) =>
handleSortClick($event, column, 'descending'),
class: 'sort-caret descending',
}),
]
),
column.filterable &&
h(FilterPanel, {
store,
placement: column.filterPlacement || 'bottom-start',
column,
upDataColumn: (key, value) => {
column[key] = value
},
}),
]
),
]
)
})
)
)
)
},
表体实现
table-body 组件
table-body 属性
| 属性 | 说明 | 类型 | 可选值 | 默认值 | |
|---|---|---|---|---|---|
| store | table内部的状态管理,类似vuex | Object | |||
| stripe | 是否带有斑马纹 | Boolean | false | ||
| tooltipEffect | tooltip effect 属性 | String | dark / light | dark | |
| context | 当前的 table 实例 | Object | {} | ||
| rowClassName | 行类名 | String | Function | ||
| rowStyle | 行 样式 | Object | Function | ||
| fixed | String | '' | |||
| highlight | 是否高亮 | Boolean | false |
table-body 的render函数
从store 中拿到数据。
遍历数据将每一行的数据转换成 vNode 渲染成 tbody的子节点。
render() {
const { wrappedRowRender, store } = this
const data = store.states.data.value || []
return h('tbody', {}, [
data.reduce((acc: VNode[], row) => {
return acc.concat(wrappedRowRender(row, acc.length))
}, []),
])
},
wrappedRowRender 方法:
- 可展开行的
vNode生成。
const hasExpandColumn = columns.some(({ type }) => type === 'expand')
// 判断是否可展开
if (hasExpandColumn) {
// 展开状态
const expanded = isRowExpanded(row)
// 当前行的 vNode
const tr = rowRender(row, $index, undefined, expanded)
// renderExpanded 方法返回 某列的 slots.default ? slots.default(data) : slots.default
const renderExpanded = parent.renderExpanded
// 判断展开状态
if (expanded) {
if (!renderExpanded) {
console.error('[Element Error]renderExpanded is required.')
return tr
}
// 渲染行
return [
[
tr,
h(
'tr',
{
key: `expanded-row__${tr.key as string}`,
},
[
h(
'td',
{
colspan: columns.length,
class: 'el-table__cell el-table__expanded-cell',
},
// 渲染 展开内容
[renderExpanded({ row, $index, store, expanded })]
),
]
),
],
]
} else {
// 使用二维数组,避免修改 $index
// Use a two dimensional array avoid modifying $index
return [[tr]]
}
}
- 树型行
vNode的生成。(暂时没看懂,后期看看v2版本的)
if (Object.keys(treeData.value).length) {
// 检查 rowKey 是否存在, 不存在就抛出异常停止执行
assertRowKey()
// TreeTable 时,rowKey 必须由用户设定,不使用 getKeyOfRow 计算
// 在调用 rowRender 函数时,仍然会计算 rowKey,不太好的操作
const key = getRowIdentity(row, rowKey.value)
// treeData 当前的节点
let cur = treeData.value[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 = [rowRender(row, $index, treeRowData)]
// 渲染嵌套数据
if (cur) {
// currentRow 记录的是 index,所以还需主动增加 TreeTable 的 index
let i = 0
const traverse = (children, parent) => {
if (!(children && children.length && parent)) return
children.forEach((node) => {
// 父节点的 display 状态影响子节点的显示状态
const innerTreeRowData = {
display: parent.display && parent.expanded,
level: parent.level + 1,
expanded: false,
noLazyChildren: false,
loading: false,
}
const childKey = getRowIdentity(node, rowKey.value)
if (childKey === undefined || childKey === null) {
throw new Error('For nested data item, row-key is required.')
}
cur = { ...treeData.value[childKey] }
// 对于当前节点,分成有无子节点两种情况。
// 如果包含子节点的,设置 expanded 属性。
// 对于它子节点的 display 属性由它本身的 expanded 与 display 共同决定。
if (cur) {
innerTreeRowData.expanded = cur.expanded
// 懒加载的某些节点,level 未知
cur.level = cur.level || innerTreeRowData.level
cur.display = !!(cur.expanded && innerTreeRowData.display)
if (typeof cur.lazy === 'boolean') {
if (typeof cur.loaded === 'boolean' && cur.loaded) {
innerTreeRowData.noLazyChildren = !(
cur.children && cur.children.length
)
}
innerTreeRowData.loading = cur.loading
}
}
i++
tmp.push(rowRender(node, $index + i, innerTreeRowData))
if (cur) {
const nodes =
lazyTreeNodeMap.value[childKey] ||
node[childrenColumnName.value]
traverse(nodes, cur)
}
})
}
// 对于 root 节点,display 一定为 true
cur.display = true
const nodes =
lazyTreeNodeMap.value[key] || row[childrenColumnName.value]
traverse(nodes, cur)
}
return tmp
}
- 普通 行
vNode生成。
rowRender(row, $index, undefined)
表格的逻辑
办了三件事!!!:
createStore创建表格 状态管理store.- 创建
TableLayout布局实例. - 暴露出 表格的可调用方法.
table内部的状态管理 store
调用getCurrentInstance api 拿到当前组件实例,将组件实例和props 创建 store 并将 store 挂载到实例上.
const table = getCurrentInstance() as Table<Row>
const store = createStore<Row>(table, props)
table.store = store
createStore 方法:
- 创建 store.
- 从将 props 中的数据 同步到 store 中的 states.
- proxyTableProps: watch props 的值,当props 值变化的时候更新 store.states
export function createStore<T>(table: Table<T>, props: TableProps<T>) {
if (!table) {
throw new Error('Table is required.')
}
const store = useStore<T>()
Object.keys(InitialStateMap).forEach((key) => {
handleValue(getArrKeysValue(props, key), key, store)
})
proxyTableProps(store, props)
return store
}
useStore 方法:
- 拿到当前的 实例.
- 设置 mutations 对象.
- 实现 commit 方法.
- 调用 useWatcher , 并暴露出其中方法 以及 state.
function useStore<T>() {
const instance = getCurrentInstance() as Table<T>
const watcher = useWatcher<T>()
type StoreStates = typeof watcher.states
const mutations = {
setData(states: StoreStates, data: T[]){},
insertColumn(
states: StoreStates,
column: TableColumnCtx<T>,
parent: TableColumnCtx<T>
) {},
removeColumn(
states: StoreStates,
column: TableColumnCtx<T>,
parent: TableColumnCtx<T>
) {},
sort(states: StoreStates, options: Sort) {},
changeSortCondition(states: StoreStates, options: Sort) {},
filterChange(_states: StoreStates, options: Filter<T>) {},
toggleAllSelection() {},
rowSelectedChanged(_states, row: T) {},
setHoverRow(states: StoreStates, row: T) {},
setCurrentRow(_states, row: T) {}
}
const commit = function (name: keyof typeof mutations, ...args) {
const mutations = instance.store.mutations
if (mutations[name]) {
mutations[name].apply(instance, [instance.store.states].concat(args))
} else {
throw new Error(`Action not found: ${name}`)
}
}
return {
ns,
...watcher,
mutations,
commit,
}
}
useWatcher 方法:
- 深度 watch state 中 的data, 当data更新的时候 如果实例存在 state 则触发布局更新.
- 暴露表格相关的方法,以及 store 的state 等.
function useWatcher<T>() {
watch(data, () => instance.state && scheduleLayout(false), {
deep: true,
})
// 检查 rowKey 是否存在
const assertRowKey = () => {
if (!rowKey.value) throw new Error('[ElTable] prop row-key is required')
}
// 更新列
const updateColumns = () => {}
// 更新 DOM
const scheduleLayout = (needUpdateColumns?: boolean, immediate = false) => {
if (needUpdateColumns) {
updateColumns()
}
if (immediate) {
instance.state.doLayout()
} else {
instance.state.debouncedUpdateLayout()
}
}
return {
assertRowKey,
updateColumns,
scheduleLayout,
isSelected,
clearSelection,
cleanSelection,
getSelectionRows,
toggleRowSelection,
_toggleAllSelection,
toggleAllSelection: null,
updateSelectionByRowKey,
updateAllSelected,
updateFilters,
updateCurrentRow,
updateSort,
execFilter,
execSort,
execQuery,
clearFilter,
clearSort,
toggleRowExpansion,
setExpandRowKeysAdapter,
setCurrentRowKey,
toggleRowExpansionAdapter,
isRowExpanded,
updateExpandRows,
updateCurrentRowData,
loadOrToggle,
updateTreeData,
states: {
tableSize,
rowKey,
data,
_data,
isComplex,
_columns,
originColumns,
columns,
fixedColumns,
rightFixedColumns,
leafColumns,
fixedLeafColumns,
rightFixedLeafColumns,
leafColumnsLength,
fixedLeafColumnsLength,
rightFixedLeafColumnsLength,
isAllSelected,
selection,
reserveSelection,
selectOnIndeterminate,
selectable,
filters,
filteredData,
sortingColumn,
sortProp,
sortOrder,
hoverRow,
...expandStates,
...treeStates,
...currentData,
},
}
}
table 的布局管理
- 创建 布局实例 并将其 挂载到
table实例上.
const layout = new TableLayout<Row>({
store: table.store,
table,
fit: props.fit,
showHeader: props.showHeader,
})
table.layout = layout
类 TableLayout:
- 实例化的时候将 store, table, fit, showHeader 传入初始化.
- observers 依赖列表, addObserver 添加依赖, removeObserver 移除依赖, notifyObservers 触发依赖更新.
- 暴露出 一些方法对 vNode 进行操作.
class TableLayout<T> {
observers: TableHeader[]
table: Table<T>
store: Store<T>
columns: TableColumnCtx<T>[]
fit: boolean
showHeader: boolean
height: Ref<null | number>
scrollX: Ref<boolean>
scrollY: Ref<boolean>
bodyWidth: Ref<null | number>
fixedWidth: Ref<null | number>
rightFixedWidth: Ref<null | number>
tableHeight: Ref<null | number>
headerHeight: Ref<null | number> // Table Header Height
appendHeight: Ref<null | number> // Append Slot Height
footerHeight: Ref<null | number> // Table Footer Height
viewportHeight: Ref<null | number> // Table Height - Scroll Bar Height
bodyHeight: Ref<null | number> // Table Height - Table Header Height
bodyScrollHeight: Ref<number>
fixedBodyHeight: Ref<null | number> // Table Height - Table Header Height - Scroll Bar Height
gutterWidth: number
constructor(options: Record<string, any>) {
for (const name in options) {
if (hasOwn(options, name)) {
if (isRef(this[name])) {
this[name as string].value = options[name]
} else {
this[name as string] = options[name]
}
}
}
if (!this.table) {
throw new Error('Table is required for Table Layout')
}
if (!this.store) {
throw new Error('Store is required for Table Layout')
}
}
updateScrollY() {}
setHeight(value: string | number, prop = 'height') {}
setMaxHeight(value: string | number) {}
getFlattenColumns(): TableColumnCtx<T>[] {}
updateElsHeight() {}
headerDisplayNone(elm: HTMLElement) {}
updateColumnsWidth() {}
addObserver(observer: TableHeader) {}
removeObserver(observer: TableHeader) {}
notifyObservers(event: string) {}
}
依赖收集:
tableBody 组件初始化的时候 通过 inject 的方式 获取到父组件 作为 root 参数传入.
export default defineComponent({
name: 'ElTableBody',
props: defaultProps,
setup(props) {
const parent = inject(TABLE_INJECTION_KEY)
const { onColumnsChange, onScrollableChange } = useLayoutObserver(parent!)
}
)}
useLayoutObserver 方法:
- 获取到当前实例.
- mount 之前将实例添加到依赖列表中.
- onMounted和 onUpdated 钩子触发时候 触发 layout 更新.
- onUnmounted 钩子移除依赖.
function useLayoutObserver<T>(root: Table<T>) {
const instance = getCurrentInstance() as TableHeader
onBeforeMount(() => {
tableLayout.value.addObserver(instance)
})
onMounted(() => {
onColumnsChange(tableLayout.value)
onScrollableChange(tableLayout.value)
})
onUpdated(() => {
onColumnsChange(tableLayout.value)
onScrollableChange(tableLayout.value)
})
onUnmounted(() => {
tableLayout.value.removeObserver(instance)
})
const tableLayout = computed(() => {
const layout = root.layout as TableLayout<T>
if (!layout) {
throw new Error('Can not find table layout.')
}
return layout
})
const onColumnsChange = (layout: TableLayout<T>) => {}
const onScrollableChange = (layout: TableLayout<T>) => {}
return {
tableLayout: tableLayout.value,
onColumnsChange,
onScrollableChange,
}
}
notifyObservers 依赖触发:
table 高度变化触发 和 列宽变化的时候会触发 notifyObservers, 这个时候他遍历 依赖列表,触发 有 state.xxx依赖的更新.
observer 就是传入的 组件实例, 所以只要在组件实例添加 这两个方法,那高度和列宽变化的时候就会触发方法达到更新.
instance.state = { onColumnsChange, onScrollableChange, }
// table 高度变化触发
this.notifyObservers('scrollable')
// table 列宽变化触发
this.notifyObservers('columns')
notifyObservers(event: string) {
const observers = this.observers
observers.forEach((observer) => {
switch (event) {
case 'columns':
observer.state?.onColumnsChange(this)
break
case 'scrollable':
observer.state?.onScrollableChange(this)
break
default:
throw new Error(`Table Layout don't have event ${event}.`)
}
})
}