胖头猫游戏提供热门游戏下载和手游攻略!

element-ui element-ui中表格组件部分源码解读及需求分析elementui官网

发布时间:2024-07-19浏览:5

前言

Element-ui开源以来,已经成为前中后台系统中最受欢迎的UI框架。

如果说 Vue、React、Angular 是前端三剑客的话,那么 element-ui 可以说占据了中后端领域的半壁江山,拥有 43k 个 github stars。截至目前,它已有 84 个组件(版本 2.13.0)。

从第 2 行开始,前两行为空。

原因

需求:由于公司业务需要,页面上经常会有需要进行多项选择(勾选)的表格,然后将勾选的ID组装成字符串提交给后台。

解决方法:查看element-ui官网的文档,可以找到在table组件中实现多选表格的方法,只需要在table组件中添加一个类型为selection的列即可。

table
    ref="multipleTable"
    :data="tableData"
    tooltip-effect="dark"
    style="width: 100%"
    @selection-change="handleSelectionChange">
    table-column
      type="selection"
      width="55">
    
table-column>
table>

通过selection-change事件,您可以获取用户选择的行数组。

效果如下:

一切看上去都很完美,但在实际使用中却出现不少问题。

为什么这么说呢?因为在公司实际业务中,表格是分页表,每次切换页码,都要重新获取数据,重新渲染表格。那么第一个问题就来了:切换页码后,页码为 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);
    });
});

嗯,没有错误,我打开页面一看,咦??为什么还是没选上!!!

我当时就想,妈的,这家伙刚刚路过……

确认了ref名称是否一致、selection内的数据是否存在、调用方法是否触发之后,还是得不到我想要的结果。

弹奏一串弦乐...

冷静了一会儿之后,我决定打开element-ui源码看看table组件是如何确定选择的?

源码分析

仅表格组件的部分源代码。

3.1 结构

首先看一下表格组件的结构

结构是这样的,最外层的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 文件夹和一些表格组件:body、column、footer、header、layout 等,一个工具文件 util.js,一个配置文件 config.js,还有一个 dropdown(看不懂),一个 layout-observer(看名字就知道是用来监听布局的),还有一个 filter-panel(用来过滤的),就这些了。

store文件夹中的代码实现了一个私有的Vuex,仅用于table组件中组件之间的数据交换。

3.2 查找

根据我的需求,我只需要查看有关选择的源代码。因此从布局开始,我可以从列开始,即 table-column.js 文件。

然而,查看了table-column.js之后,确实有一些关于列的内容,但是我找不到选择功能的任何代码。

于是我暂时放弃在布局中搜索,直接在方法 toggleRowSelection 中搜索。在这个表格文件夹中,我搜索了关键字 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方法获取变化的值,然后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)则添加,否则移除。主要就是获取索引值,很简单粗暴,直接用Array.prototype.indexOf判断就行了。想想是不是这个原因?

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

陷阱1:如果这个元素是一个对象的话,要知道对象是引用类型,也就是说用indexOf来判断只能判断对象引用的地址是否相同,而不能判断里面的值是否相同。

不过我仔细想了想好像这对表格没什么影响,我们来详细想一下:我们初始化表格,有10条数据,此时表格组件中用来存储选中行的数组selection一开始是空的,然后我们调用toggleRowSelection主动设置选中行,并把这些行放入表格组件中的selection中(通过toggleRowStatus中的addRow方法)。

已经放进去了,为什么不渲染出相应的状态!!?

因为我们知道表格组件是通过选择来存储选中的行的,所以我们来搜索选择。

我们在 7 个文件中得到了 78 个结果。结果太多了,我们不希望这样。

element-ui_elementui中文官网_elementui官网

然后进一步进行完全匹配搜索:

在 5 个文件中找到 38 条结果。

我稍微缩小了范围,但还是有很多,没办法,只能一个一个的找。

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

这个关键字在config.js文件中出现了4次,可以看到前两次匹配的结果都是一种样式,这并不是我们想要的。

最后两次值得一看。

// config.js 29行
// 这些选项不应该被覆盖
export const cellForced = {
  selection: {
    renderHeader: function(h, { store }) {
      return         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         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行,这才是我们想要的选择。

不确定={store.states.selection.length>0&&!this.isAllSelected}

分析:store.states.selection:我是包含选定行的数组集合。

事实上,如果我们查看搜索结果中的第三个文件:watcher.js,我们可以清楚地找到它:

而在第五个文件:table.vue中,mapStates用来进行地图选择,同样能找到它的影子:

然后我们就不需要再关心这两个文件了,因为我们已经找到了布局的位置,返回config.js:

// config.js 29行
// 这些选项不应该被覆盖
export const cellForced = {
  selection: {
    renderHeader: function(h, { store }) {
      return         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         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中的一个方法。

好的,找到store文件夹,搜索isSelected关键字。我们在watcher.js中找到了它:

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

里面的逻辑就更简单了,就是取出包含选中行的选择数组,然后返回该行在选择中的位置是否大于-1,根据渲染函数中的内容判断,如果返回值为true,则渲染为选中,否则为未选中;

陷阱2:再次使用indexOf来判断对象是否在数组中。

这个地方很致命,为什么我这么说呢?

因为selection确实存储了toggleRowSelection设置的row。但是在isSelected中,参数row是从table组件中的props中的数据传递过来的。

数据是通过重新请求接口来获取的,所以data里的行和selection里存储的行不是同一行。

这句话听上去好绕啊,我们回到最基本的,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的structure和id是相同的,所以这两个是同一个东西。

我给你举个更现实的例子:你二叔从村里的瓦房里走出来,你认出他是你二叔;你看见你二叔从北京东二环某小区某单位走出来,你二叔不是你二叔。这简直可笑!!

所以,这就说明,表格组件渲染源码时的判断太简单了,并没有做更深层次的判断,只是对比了一下引用地址是否相同。

一旦找到问题的根源,解决它就很容易了。

解决方案

上面的1、2、3、4方法我都实现过了,根据具体业务需求不同而不同。

我只实现了一层深度比较,我的想法是先比较key值的数量,然后判断你二叔的key到你大叔的属性是否存在,值是否相等,这是因为我的业务数据只有一层属性值。

4.1 第一种方法是自己渲染一个新的el-checkbox(shaonialife提出):

// 通过自己渲染一个新的<el-checkbox />
<el-table-column
  :align="tableColumn.align"
  type="index"
  label="序号"
  width="70">

  <template slot-scope="scope">
    <el-checkbox :value="!!checkedRowIds[scope.row.id]" @change="(val) => { toggleRowSelection(val, scope.row) }"/>
  template>
el-table-column>

checkedRowIds 是一个包含带有 id 的键的集合的对象,如下所示:

const checkedRowIds = {
    0true,
    1true,
    2false
}

当row.id = 1时,checkedRowIds[row.id]相当于checkedRowIds.1,该值为true,为true则渲染checked状态。

然后我们看一下change事件的回调函数toggleRowSelection

toggleRowSelection(val, row) {
  const { checkedRowIds } = this.$props
  const { id } = row

  this.$set(checkedRowIds, id, val)

  const includes = checkedRowIds.hasOwnProperty(id)

  const remove = () => delete checkedRowIds[id]
  if (!val && includes) remove()

  const keys = Object.keys(checkedRowIds)
  const arrToString = arr => arr.join(',')

  const ids = arrToString(keys)
  // ids: 1,2,3
},

首先解构获取checkedRowIds和row.id,然后使用this.$set方法将键为id,值是val的项设置到checkedRowIds对象集合中。

然后判断val是否为false,以及key是否在checkedRowIds中存在,如果两个条件都满足,则删除该属性。最后通过Object.keys()获取checkedRowIds中所有key的集合,再通过join()方法组成字符串ids。

分析:这个方法很棒,!!checkedRowIds[scope.row.id]很短小精悍,然后进入页面的时候默认勾选就很方便了,只要checkedRowIds里面有一个就会勾选,不用再调用toggleRowSelection设置了。

4.2 第二种方法,采用row-key和reserve-selection相结合的方式(Han__提出)

// 利用row-key与reserve-selection的组合拳
 table
  v-loading="loading"
  :data="tableData"
  row-key="id"
  @selection-change="rowChange"
>

  table-column
    :reserve-selection="true"
    type="selection"
    width="55"/>

table>

将row-key设置为id之后,每一行都有一个非唯一的键值,然后通过reserve-selection字段保留每次选择的结果,这样不管页码如何切换,都能保留用户先前选择的值。

分析:这种方法很简单,主要用了官方的属性,不用改太多代码,这点很好。但是进入页面后,要显示默认勾选的行,需要和tableData中的数据进行对比,然后通过toggleRowSelection方法进行设置。

写在后面

我之所以能成功,是因为我站在巨人的肩膀上。————牛顿

非常感谢大家的集思广益,评论区的每一条留言我都认真看,每一个方案我都会去实现,谢谢大家,希望在这条路上我能走得更远。

热点资讯