郭涛

V1

2022/07/06阅读:66主题:橙心

elementPlus表格组件粗览

表格组件

表格结构

表头的实现

表头的外层为table 内部包含 hColgrouptable-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-bodyrender函数

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 方法:

  1. 可展开行的 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]]
      }
    }
  1. 树型行 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,
          displaytrue,
        }
        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,
              expandedfalse,
              noLazyChildrenfalse,
              loadingfalse,
            }
            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
    }
  1. 普通 行 vNode 生成。
rowRender(row, $index, undefined)

表格的逻辑

办了三件事!!!:

  1. createStore 创建表格 状态管理 store .
  2. 创建 TableLayout 布局实例.
  3. 暴露出 表格的可调用方法.

table内部的状态管理 store

调用getCurrentInstance api 拿到当前组件实例,将组件实例和props 创建 store 并将 store 挂载到实例上.

const table = getCurrentInstance() as Table<Row>
const store = createStore<Row>(table, props)
table.store = store

createStore 方法:

  1. 创建 store.
  2. 从将 props 中的数据 同步到 store 中的 states.
  3. 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 方法:

  1. 拿到当前的 实例.
  2. 设置 mutations 对象.
  3. 实现 commit 方法.
  4. 调用 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 方法:

  1. 深度 watch state 中 的data, 当data更新的时候 如果实例存在 state 则触发布局更新.
  2. 暴露表格相关的方法,以及 store 的state 等.

function useWatcher<T>({
  watch(data, () => instance.state && scheduleLayout(false), {
    deeptrue,
  })

  // 检查 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,
    toggleAllSelectionnull,
    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 的布局管理

  1. 创建 布局实例 并将其 挂载到 table 实例上.
const layout = new TableLayout<Row>({
    store: table.store,
    table,
    fit: props.fit,
    showHeader: props.showHeader,
})
table.layout = layout

TableLayout:

  1. 实例化的时候将 store, table, fit, showHeader 传入初始化.
  2. observers 依赖列表, addObserver 添加依赖, removeObserver 移除依赖, notifyObservers 触发依赖更新.
  3. 暴露出 一些方法对 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 方法:

  1. 获取到当前实例.
  2. mount 之前将实例添加到依赖列表中.
  3. onMounted和 onUpdated 钩子触发时候 触发 layout 更新.
  4. 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}.`)
      }
    })
  }

分类:

前端

标签:

前端

作者介绍

郭涛
V1