解读element-ui中table组件部分源码与需求分析

1、前言

element-ui开源至今已成为前端在中后台系统中最为热门的ui框架了。html

若是说Vue、React、Angular是前端三剑客,那么element-ui能够说在中后台领域占据半壁江山,github star数 43k之多。至今,它拥有了84个组件(Version 2.13.0)。前端

前两行是空的,从第2行开始。vue

2、原由

需求:因公司业务须要,常常有页面中的表格须要多选(勾选),而后把勾选到的id组装拼成字符串提交到后台。git

解决方案:在element-ui官网看文档,可以在table组件找到实现多选表格的办法,在table组件中加一个typeselection的列就好了。github

<el-table ref="multipleTable" :data="tableData" tooltip-effect="dark" style="width: 100%" @selection-change="handleSelectionChange">
    <el-table-column type="selection" width="55">
    </el-table-column>
</el-table>
复制代码

配合selection-change事件,能够得到用户选中的row组成的数组。element-ui

效果以下:数组

一切看起来都很完美,可是在实际运用中情况是千奇百出。数据结构

为何这么说?由于在公司的实际业务中,表格是分页表格,每次切换页码,数据从新获取,表格从新渲染,那么第一个问题来了:用户在页码为1的表格选中的行,在切换页码以后,不见了。框架

分页表格应该像下面这样:函数

因而我又去element-ui官网翻看文档,在table组件中找到了一个方法toggleRowSelection,此方法能够切换表格中具体哪一行的选中状态。

经过这个方法,咱们在获取表格数据以后,立刻用此方法设置以前选中过的数据,这样不就能够在用户切换的时候也把以前选中的行选中状态渲染出来了吗?

坑又立刻来了!!

由于经过selection-change事件获取到了一个名为selection的数组,里面包含了用户选中的行的信息。咱们把这个数组保存在一个变量中,用于用户切换页码以后还能看见以前选中的行,然经过toggleRowSelection方法设置行的选中信息。

selection.forEach(row => {
    this.$refs.multipleTable.toggleRowSelection(row);
});
复制代码

这乍一看是没有问题,可是在表格中,它竟然没有勾选效果!!?

各类一度度娘,说是要在nextTick中去调用这个方法:

this.$nextTick(() => {
    selection.forEach(row => {
        this.$refs.multipleTable.toggleRowSelection(row);
    });
});
复制代码

嗯,没报错,打开页面一看,嗯??怎么仍是没有选中!!!

内心一w个草尼玛路过。。。。

在肯定ref名称是否一致、selection中数据是否存在、调用方法是否触发以后,我仍旧得不到我想要的结果。

玩个串串。。。

一阵冷静事后,我决定了,打开element-ui源码看一看table组件中是如何判断选中的?

3、源码分析

仅仅是table组件部分源码。

3.1 结构

首先看下table组件的结构

结构就是这样,最外层的index.js用于导出这个table模块,里面的代码也很是简单,确定能看懂的。

// index.js
import ElTable from './src/table';

/* istanbul ignore next */
ElTable.install = function(Vue) {
  Vue.component(ElTable.name, ElTable);
};

export default ElTable;
复制代码

而后src里面包含一个store文件夹和一些table的组件:body、column、footer、header、layout等,工具类文件util.js,配置文件config.js,and 一个dropdown(没懂)、一个layout-observer(从名字上看是监听layout的)、filter-panel(过滤用的)大概就这样。

store文件夹里面的代码就是实现了一个只用于table组件中各组件数据交换的一个私有的Vuex。

3.2 找到它

按照个人需求,我只须要看部分关于selection的源码。因此从布局上,我能够先从列从手,也就是table-column.js这文件。

但是看了下table-column.js里边确实是关于列的一些内容,可是从字面意思上没找到selection部分的功能的代码。

因此我暂且放弃从布局上找,我直接从方法上找:toggleRowSelection。在这个table文件夹中用搜索大法,直接搜关键词toggleRowSelection,在src/store/watcher.js中能够找到以下:

// watcher.js 158行
toggleRowSelection(row, selected, emitChange = true) {
  const changed = toggleRowStatus(this.states.selection, row, selected);
  if (changed) {
    const newSelection = (this.states.selection || []).slice();
    // 调用 API 修改选中值,不触发 select 事件
    if (emitChange) {
      this.table.$emit('select', newSelection, row);
    }
    this.table.$emit('selection-change', newSelection);
  }
}
复制代码

这个方法就是暴露在外部供咱们调用的,里面第一行是主要信息,调用toggleRowStatus方法而后获得changed值,而后把这个值emit出去。大概是这么个过程,那么就要从toggleRowStatus着手了。

注意第一行中的 this.states.selection将是后续的关键。

直接搜索关键词,能够找到这个方法是外部导出引用进来的。

import { getKeysMap, getRowIdentity, getColumnById, getColumnByKey, orderBy, toggleRowStatus } from '../util';
复制代码

打开util.js文件,顺利的找到了如下代码:

export function toggleRowStatus(statusArr, row, newVal) {
  let changed = false;
  const index = statusArr.indexOf(row);
  const included = index !== -1;

  const addRow = () => {
    statusArr.push(row);
    changed = true;
  };
  const removeRow = () => {
    statusArr.splice(index, 1);
    changed = true;
  };

  if (typeof newVal === 'boolean') {
    if (newVal && !included) {
      addRow();
    } else if (!newVal && included) {
      removeRow();
    }
  } else {
    if (included) {
      removeRow();
    } else {
      addRow();
    }
  }
  return changed;
}
复制代码

解读起来也不是很难,方法名字面意思:切换行的状态。里面有两个方法,一个addRow,一个removeRow,都是字面意思。 主要实现功能:判断下是不是新的值(newVal),若是不存在(!included)就add,反之remove。主要是index值的获取,很简单粗暴,直接用Array.prototype.indexOf去判断,思考下,缘由是否是在这里?

Array.prototype.indexOf():方法返回在数组中能够找到一个给定元素的第一个索引,若是不存在,则返回-1。

  • 坑1:若是这个元素是一个对象(Object),你们应该知道对象是引用类型,也就是说用indexOf去判断,只能判断出对象引用的地址是否是同样的,并不能判断里面的值是否是同样的。

可是,我仔细考虑了下,这里好像并不影响。具体思考下:咱们初始化表格10条数据,此时table组件中用于存放选中row的数组selection这玩意一开始是空的,而后咱们调用toggleRowSelection主动设置被选中的row,这些row都被放进到了table组件中的selection中了(经过这个toggleRowStatus中addRow方法)。

已经放进去,为何不渲染相应的状态!!?

既然知道,table组件是经过selection存放被选中的row,那么,就搜索selection吧。

获得了78个结果,在7个文件中。获得的结果太多了,咱们不想要这样的结果。

而后进一步用全匹配搜索:

获得了38个结果,在5个文件中。

缩小了一些范围,可是仍是不少,也没办法了。一个一个文件找。

纯按照Vscode给我搜出来的顺序,第一个文件是config.js文件。

这个关键词在config.js文件中出现了4次,能够看到前面两次匹配结果是一个样式,并非咱们要的东西。

后面两次就很值得看了。

// config.js 29行
// 这些选项不该该被覆盖
export const cellForced = {
  selection: {
    renderHeader: function(h, { store }) {
      return <el-checkbox
        disabled={ store.states.data && store.states.data.length === 0 }
        indeterminate={ store.states.selection.length > 0 && !this.isAllSelected }
        nativeOn-click={ this.toggleAllSelection }
        value={ this.isAllSelected } />;
    },
    renderCell: function(h, { row, column, store, $index }) {
      return <el-checkbox
        nativeOn-click={ (event) => event.stopPropagation() }
        value={ store.isSelected(row) }
        disabled={ column.selectable ? !column.selectable.call(null, row, $index) : false }
        on-input={ () => { store.commit('rowSelectedChanged', row); } } />;
    },
    sortable: false,
    resizable: false
  }
  // ...省略
}
复制代码

只贴出有用的,从大的耳朵看,导出了一个模块叫cellForced,虽然我不知道什么意思。(四级没过,砸砸辉)。 可是里面两个函数我可看懂了,看到了render关键词,这不就是渲染的意思嘛,再往里一看,妈呀,幸福!!里面竟然有el-checkbox这个组件,这不就是多选模式下那一列吗?(除此以外在table中别的地方不可能放玩意!)。

其实只有第四个关键词出现的位置,在第34行才是咱们想要的selection这玩意。

indeterminate={ store.states.selection.length > 0 && !this.isAllSelected }

分析一下: store.states.selection: 我才是里面装有被选中row的数组集合。

其实接着看搜索结果第三个文件:watcher.js中,很明显能找到它:

而且在第五个文件:table.vue中使用了mapStates去映射selection,也能够找到它的影子:

而后这两个文件不用管了,由于咱们找到了布局的位置,回到config.js中:

// config.js 29行
// 这些选项不该该被覆盖
export const cellForced = {
  selection: {
    renderHeader: function(h, { store }) {
      return <el-checkbox
        disabled={ store.states.data && store.states.data.length === 0 }
        indeterminate={ store.states.selection.length > 0 && !this.isAllSelected }
        nativeOn-click={ this.toggleAllSelection }
        value={ this.isAllSelected } />;
    },
    renderCell: function(h, { row, column, store, $index }) {
      return <el-checkbox
        nativeOn-click={ (event) => event.stopPropagation() }
        value={ store.isSelected(row) }
        disabled={ column.selectable ? !column.selectable.call(null, row, $index) : false }
        on-input={ () => { store.commit('rowSelectedChanged', row); } } />;
    },
    sortable: false,
    resizable: false
  }
  // ...省略
}
复制代码

一共使用了两个渲染函数,一个渲染头部,一个渲染格子,经过el-checkbox组件的属性值咱们能够判断出在41行中:

value={ store.isSelected(row) }
复制代码

这一行才是渲染选中与否的关键所在。里面逻辑简单,就调用了一个方法名叫:isSelected,还告诉了咱们是store中的方法。

ok,找到store文件夹,搜索一下isSelected关键词,在watcher.js中,咱们找到了它:

// watcher.js 120行
// 选择
isSelected(row) {
  const { selection = [] } = this.states;
  return selection.indexOf(row) > -1;
},
复制代码

里边的逻辑更是简单的一匹,取出selection这个存有选中row的数组,而后返回row在selection中的位置是否大于-1。联系渲染函数中的内容,返回值为true就渲染选中,反之不选中;

  • 坑2: 又是用的indexOf判断一个对象是否在数组中。

这里十分的致命,为何这么说?

由于selection中确实存放了经过toggleRowSelection设置进来的row。可是在isSelected形参row是从table组件中的props中的data传递过来的。

data又是从新请求接口得到的,因此在data中的row和selection中存放的row,它不是一个row。

这句话听上去怎么这么绕,回到最基础的,row是一个对象,它是一个引用类型,只要引用的地址不同,那么你就不是你了。

虽然在数据结构和内容上,这两个row都同样,假设都是如下的玩意:

const row1 = { name: '1', id: 0, code: 110110, area: '北京市', street: '二环' }
const row2 = { name: '1', id: 0, code: 110110, area: '北京市', street: '二环' }
复制代码

row1和row2他喵的不相等。

可是根据咱们的实际业务,row1和row2结构同样,id同样,这两个玩意就是一个东西。

举个更实际的例子:你二舅在村里的瓦房里出来,你认出来了是你二舅;你在北京东二环的某个小区里看见你二舅从某个单元出来,你二舅他喵的不是你的二舅了。这太扯了!!

因此,意思就是说在table组件中源码渲染的时候的判断,太简单了,没有更深的判断,只比较引用地址是否相同。

找到问题的根本所在,解决起来也是至关的容易。

4、解决方案

  • 1.等element-ui更新,有人解决bug(在下不才,贡献了代码,还没过审核好像)
  • 2.将保存在变量中的row和当前获得的table的data中的row进行深度比对,获得选中的row在当前tableData中的位置,而后将使用toggleRowSelection(tableData[index])的形式,确保你二舅是你二舅。
  • 3.自行封装多选表格组件,不用自带的selection,而是经过本身实现这个功能。
  • 4.改写Array.prototype.indexOf方法,使内部判断逻辑在对象的时候进行深度比对。

以上办法,1,2,3,4我都实现了,根据具体业务需求而变化。

深度比对,我也只是实现了一层。个人思路,首先对比key值数量,而后判断你二舅的key给你大舅,这属性是否存在,里面的值是否相等。由于个人业务数据只有一层的属性值。

后续更新,将放上解决方案的代码,但愿可以对你们有所帮助。

相关文章
相关标签/搜索