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

4,325 阅读5分钟

最近有与业务需要,需要实现表格中固定列的功能,于是顺便研究了一下Element UI 的table组件,这里记录一下。

整体结构

话不多说,先看代码

  • store文件夹:为table设计了一组私有的store数据,类似(vuex, redux),这个一会详细讲。
  • config.js: 一些配置和默认信息,包括默认宽度之类的
  • dropdown.js: 提供点击后产生dropdown的一些基础方法
  • filter-panel.vue: 渲染过滤面板的
  • layout-observer.js: 布局使用的一个Observer,里面提供了一些基础方法,主要包括两点:1.column变化时,动态更新显示宽度,2.table在进行滚动时,计算滚动位置。
  • table-body, table-column, table-footer, table-header,这个四个顾名思义都是分别负责渲染对应的body,column,footer,header
  • table-layout.js: 定义了一个TableLayout的基础类,内部建立了一个观察者模式。
  • table.vue: 组合上面几个渲染模块,渲染整个table组件
  • util.js: 一些工具方法

Store

熟悉vuex和redux的人都知道store的概念,由于表格数据比较复杂,element为table设计了一套私有的store便于管理,header, body等模块对store数据进行监听,table-store数据发生变化时,监听的模块自动更新渲染,这样使数据流向更清晰,也符合“数据驱动”的设计理念。

看一下store/index.js

import Watcher from './watcher';

这里首先引用了一个watcher,看看watcher做了什么:

export default Vue.extend({
  data() {
    return {
        state:...
    }
  },
  methods() {
    // 检查 rowKey 是否存在
    assertRowKey() {},
    // 更新列
    updateColumns() {
      ...
      states.columns = ...;
    },

    // 更新 DOM
    scheduleLayout(needUpdateColumns) {
      if (needUpdateColumns) {
        this.updateColumns();
      }
      this.table.debouncedUpdateLayout();
    },

    // 是否选中
    isSelected(row) {},
    // 清除选中
    clearSelection() {},
    // 清理选中信息(当数据实例发生改变时)
    cleanSelection() {},
    // 修改选中信息
    toggleRowSelection(row, selected, emitChange = true) {},
    // 修改全选信息
    _toggleAllSelection() {},
    // 根据rowkey更新选中信息
    updateSelectionByRowKey() {},
    // 更新全选信息
    updateAllSelected() {},
    // 更新过滤信息,过滤返回一个过滤器
    updateFilters(columns, values) {},
    // 更新排序信息
    updateSort(column, prop, order) {},
    // 根据过滤器, 执行过滤筛选操作
    execFilter() {},
    // 执行排序操作
    execSort() {},

    // 根据 filters 与 sort 去过滤 data
    execQuery(ignore) {},
    // 清理过滤器
    clearFilter(columnKeys) {},
    // 清理排序
    clearSort() {},
    // 适配层,expand-row-keys 在 Expand 与 TreeTable 中都有使用
    setExpandRowKeysAdapter(val) {},

    // 展开行与 TreeTable 都要使用
    toggleRowExpansionAdapter(row, expanded) {}
  }
})

好了这里知道Watcher实际上是利用vue构造出的一个子类,提供了state状态包括列数据、选择数据、过滤数据、排序数据等,并且提供了一些列数据处理的方法(排序,过滤等)。

再回到index文件:

Watcher.prototype.mutations = {
  // 设置data数据
  setData(states, data) {},
  // 插入列 插入修改列数组,并通知table重新渲染
  insertColumn(states, column, index, parent) {},
  // 删除列 删除列数组信息,并通知table重新渲染
  removeColumn(states, column, parent) {},
  // 对列进行排序
  sort(states, options) {},
  // 修改排序条件
  changeSortCondition(states, options) {},
  // 修改过滤器
  filterChange(states, options) {},
  // 处理全选
  toggleAllSelection() {},
  // 处理行选中
  rowSelectedChanged(states, row) {},
  // 设置hover的行
  setHoverRow(states, row) {},
  // 设置当前行 调用current.js里面的方法
  setCurrentRow(states, row) {}
};
Watcher.prototype.commit = function(name, ...args) {}
Watcher.prototype.updateTableScrollY = function() {}
export default Watcher;

这里为watcher添加了mutations的原型方法,熟悉store设计理念的都能理解,修改store数据必须要通过mutation方法保证数据流向的清晰。

current.js

current.js主要是处理current row变化的方法,row数据变更,以及事件的抛出。

export default {
  data() {
    return {
      states: {
        // 不可响应的,设置 currentRowKey 时,data 不一定存在,也许无法算出正确的 currentRow
        // 把该值缓存一下,当用户点击修改 currentRow 时,把该值重置为 null
        _currentRowKey: null,
        currentRow: null
      }
    };
  },

  methods: {
    // 设置当前行Key
    setCurrentRowKey(key) {},
    // 重置当前行key
    restoreCurrentRowKey() {},
    // 通过key找到当前行数据
    setCurrentRowByKey(key) {},
    // 更新当前行变化,抛出事件
    updateCurrentRow(currentRow) {},
    // 更新当前行变化的数据
    updateCurrentRowData() {}
};

expand.js

expand文件主要提供处理扩展行的几个方法,分析如下

export default {
  data() {
    return {
      states: {
        defaultExpandAll: false,
        expandRows: []
      }
    };
  },

  methods: {
    // 更新扩展行信息
    updateExpandRows() {},
    // 控制是否显示扩展行,并抛出事件
    toggleRowExpansion(row, expanded) {},
    // 设置扩展行的数据和key
    setExpandRowKeys(rowKeys) {},
    // 判断是否为扩展行
    isRowExpanded(row) {}
  }
};

helper.js

helper这里理解为提供createStore, mapStates两个方法, createStore方法主要是创建store对象,而mapState提供了把state映射到实例数据上的功能。在table.vue中使用创建store

import Store from './index';

export function createStore(table, initialState = {}) {}

export function mapStates(mapper) {};

tree.js

tree.js主要是提供了树形数据的节点更新,序列化等方法,mixin到index文件中去,这里不具体展开。

最后看一下主要Table组件

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>

引用的文件包括:

//checkbox组件
import ElCheckbox from 'element-ui/packages/checkbox';
//节流,防抖
import { debounce, throttle } from 'throttle-debounce';
//resize的事件监听器
import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
// 鼠标滚动处理方法
import Mousewheel from 'element-ui/src/directives/mousewheel';
// 多语言
import Locale from 'element-ui/src/mixins/locale';
// 低版本的移植提示
import Migrating from 'element-ui/src/mixins/migrating';
// 刚才提到的store数据
import { createStore, mapStates } from './store/helper';
// table布局方法
import TableLayout from './table-layout';
// 渲染Body
import TableBody from './table-body';
// 渲染表头
import TableHeader from './table-header';
// 渲染表尾
import TableFooter from './table-footer';
// px高度转换方法
import { parseHeight } from './util';

创建store数据在data部分:

this.store = createStore(this, {
    rowKey: this.rowKey,
    defaultExpandAll: this.defaultExpandAll,
    selectOnIndeterminate: this.selectOnIndeterminate,
    // TreeTable 的相关配置
    indent: this.indent,
    lazy: this.lazy,
    lazyColumnIdentifier: hasChildren,
    childrenColumnName: children
});
const layout = new TableLayout({
    store: this.store,
    table: this,
    fit: this.fit,
    showHeader: this.showHeader
});
return {
    layout,
    isHidden: false,
    renderExpanded: null,
    resizeProxyVisible: false,
    resizeState: {
      width: null,
      height: null
    },
    // 是否拥有多级表头
    isGroup: false,
    scrollPosition: 'left'
};

最后在data中生成store对象,再初始化一个tableLayout对象,tableLayout提供了对整个表格宽度,高度等进行布局修改的方法,本篇文章不详细展开。