解讀element-ui中table組件部分源碼與需求分析

前言

element-ui開源至今已成為前端在中後臺系統中最為熱門的ui框架了。

如果說Vue、React、Angular是前端三劍客,那麼element-ui可以說在中後臺領域佔據半壁江山,github star數 43k之多。至今,它擁有了84個組件(Version 2.13.0)。

解讀element-ui中table組件部分源碼與需求分析

前兩行是空的,從第2行開始。

起因

需求:因公司業務需要,經常有頁面中的表格需要多選(勾選),然後把勾選到的id組裝拼成字符串提交到後臺。解決方案:在element-ui官網看文檔,能夠在table組件找到實現多選表格的辦法,在table組件中加一個type為selection的列就行了。

<code><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>/<code>

配合selection-change事件,可以獲得用戶選中的row組成的數組。效果如下:

解讀element-ui中table組件部分源碼與需求分析

一切看起來都很完美,但是在實際運用中狀況是千奇百出。

為什麼這麼說?因為在公司的實際業務中,表格是分頁表格,每次切換頁碼,數據重新獲取,表格重新渲染,那麼第一個問題來了:用戶在頁碼為1的表格選中的行,在切換頁碼之後,不見了。分頁表格應該像下面這樣:

解讀element-ui中table組件部分源碼與需求分析

於是我又去element-ui官網翻看文檔,在table組件中找到了一個方法toggleRowSelection,此方法可以切換表格中具體哪一行的選中狀態。

解讀element-ui中table組件部分源碼與需求分析

通過這個方法,我們在獲取表格數據之後,馬上用此方法設置之前選中過的數據,這樣不就可以在用戶切換的時候也把之前選中的行選中狀態渲染出來了嗎?


坑又馬上來了!!

因為通過selection-change事件獲取到了一個名為selection的數組,裡面包含了用戶選中的行的信息。我們把這個數組保存在一個變量中,用於用戶切換頁碼之後還能看見之前選中的行,然通過toggleRowSelection方法設置行的選中信息。

<code>selection.forEach(row => {
    this.$refs.multipleTable.toggleRowSelection(row);
});/<code>

這乍一看是沒有問題,但是在表格中,它居然沒有勾選效果!!?各種一度度娘,說是要在nextTick中去調用這個方法:

<code>this.$nextTick(() => {
    selection.forEach(row => {
        this.$refs.multipleTable.toggleRowSelection(row);
    });
});/<code>

嗯,沒報錯,打開頁面一看,嗯??怎麼還是沒有選中!!!

心裡一w個草尼瑪路過。。。。

在確定ref名稱是否一致、selection中數據是否存在、調用方法是否觸發之後,我仍舊得不到我想要的結果。玩個串串。。。一陣冷靜過後,我決定了,打開element-ui源碼看一看table組件中是如何判斷選中的?

源碼分析

僅僅是table組件部分源碼。

3.1 結構

首先看下table組件的結構

結構就是這樣,最外層的index.js用於導出這個table模塊,裡面的代碼也非常簡單,肯定能看懂的。

<code>// index.js
import ElTable from './src/table';

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

export default ElTable;/<code>

然後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中可以找到如下:

<code>// 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);
  }
}/<code>

這個方法就是暴露在外部供我們調用的,裡面第一行是主要信息,調用toggleRowStatus方法然後得到changed值,然後把這個值emit出去。大概是這麼個過程,那麼就要從toggleRowStatus著手了。

注意第一行中的 this.states.selection將是後續的關鍵。

直接搜索關鍵詞,可以找到這個方法是外部導出引用進來的。

<code>import { getKeysMap, getRowIdentity, getColumnById, getColumnByKey, orderBy, toggleRowStatus } from '../util';/<code>

打開util.js文件,順利的找到了以下代碼:

<code>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;
}/<code>

解讀起來也不是很難,方法名字面意思:切換行的狀態。裡面有兩個方法,一個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吧。

解讀element-ui中table組件部分源碼與需求分析

得到了78個結果,在7個文件中。得到的結果太多了,我們不想要這樣的結果。
然後進一步用全匹配搜索:

解讀element-ui中table組件部分源碼與需求分析


得到了38個結果,在5個文件中。
縮小了一些範圍,但是還是很多,也沒辦法了。一個一個文件找。
純按照Vscode給我搜出來的順序,第一個文件是config.js文件。

解讀element-ui中table組件部分源碼與需求分析


這個關鍵詞在config.js文件中出現了4次,可以看到前面兩次匹配結果是一個樣式,並不是我們要的東西。
後面兩次就很值得看了。

<code>// 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>/<el-checkbox>/<code>

只貼出有用的,從大的耳朵看,導出了一個模塊叫cellForced,雖然我不知道什麼意思。(四級沒過,砸砸輝)。但是裡面兩個函數我可看懂了,看到了render關鍵詞,這不就是渲染的意思嘛,再往裡一看,媽呀,幸福!!裡面居然有el-checkbox這個組件,這不就是多選模式下那一列嗎?(除此之外在table中別的地方不可能放玩意!)。

其實只有第四個關鍵詞出現的位置,在第34行才是我們想要的selection這玩意。

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

分析一下:store.states.selection: 我才是裡面裝有被選中row的數組集合。其實接著看搜索結果第三個文件:watcher.js中,很明顯能找到它:

解讀element-ui中table組件部分源碼與需求分析


並且在第五個文件:table.vue中使用了mapStates去映射selection,也可以找到它的影子:

解讀element-ui中table組件部分源碼與需求分析


然後這兩個文件不用管了,因為我們找到了佈局的位置,回到config.js中:

<code>// 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>/<el-checkbox>/<code>

一共使用了兩個渲染函數,一個渲染頭部,一個渲染格子,通過el-checkbox組件的屬性值我們可以判斷出在41行中:

<code>value={ store.isSelected(row) }/<code>

這一行才是渲染選中與否的關鍵所在。裡面邏輯簡單,就調用了一個方法名叫:isSelected,還告訴了我們是store中的方法。ok,找到store文件夾,搜索一下isSelected關鍵詞,在watcher.js中,我們找到了它:

解讀element-ui中table組件部分源碼與需求分析

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

裡邊的邏輯更是簡單的一匹,取出selection這個存有選中row的數組,然後返回row在selection中的位置是否大於-1。聯繫渲染函數中的內容,返回值為true就渲染選中,反之不選中;

坑2: 又是用的indexOf判斷一個對象是否在數組中。

這裡十分的致命,為什麼這麼說?因為selection中確實存放了通過toggleRowSelection設置進來的row。但是在isSelected中形參row是從table組件中的props中的data傳遞過來的。data又是重新請求接口獲得的,所以在data中的row和selection中存放的row,它不是一個row。這句話聽上去怎麼這麼繞,回到最基礎的,row是一個對象,它是一個引用類型,只要引用的地址不一樣,那麼你就不是你了。雖然在數據結構和內容上,這兩個row都一樣,假設都是以下的玩意:

<code>const row1 = { name: '1', id: 0, code: 110110, area: '北京市', street: '二環' }
const row2 = { name: '1', id: 0, code: 110110, area: '北京市', street: '二環' }/<code>

row1和row2他喵的不相等。

解讀element-ui中table組件部分源碼與需求分析


但是根據我們的實際業務,row1和row2結構一樣,id一樣,這兩個玩意就是一個東西。
舉個更實際的例子:你二舅在村裡的瓦房裡出來,你認出來了是你二舅;你在北京東二環的某個小區裡看見你二舅從某個單元出來,你二舅他喵的不是你的二舅了。這太扯了!!


所以,意思就是說在table組件中源碼渲染的時候的判斷,太簡單了,沒有更深的判斷,只比較引用地址是否相同。
找到問題的根本所在,解決起來也是相當的容易。


解決方案

  • 等element-ui更新,有人解決,提issue。
  • 將保存在變量中的row和當前得到的table的data中的row進行深度比對,得到選中的row在當前tableData中的位置,然後將使用toggleRowSelection(tableData[index])的形式,確保你二舅是你二舅。
  • 自行封裝多選表格組件,不用自帶的selection,而是通過自己實現這個功能(可以參考評論區shaonialife童鞋的方法,非常棒)。
  • 改寫Array.prototype.indexOf方法,使內部判斷邏輯在對象的時候進行深度比對。
  • 通過設置el-table中的row-key為id,然後設置type="selection"的那一列的reserve-selection為true,這樣在你切換頁碼的時候,之前的頁碼選中的值會被保留(由評論區的 晗__ 小夥伴提出,感謝)。但是還是要注意一點,如果需要默認勾選,依舊要判斷默認勾選的那幾個row在數據源tableData中的位置,然後通過toggleRowSelection設置進去。

以上辦法,1,2,3,4我都實現了,根據具體業務需求而變化。

深度比對,我也只是實現了一層。我的思路,首先對比key值數量,然後判斷你二舅的key給你大舅,這屬性是否存在,裡面的值是否相等。因為我的業務數據只有一層的屬性值。

4.1 第一種通過自己渲染一個新的el-checkbox(shaonialife童鞋提出):

<code>// 通過自己渲染一個新的<el-checkbox>
<el-table-column>  :align="tableColumn.align"
  type="index"
  label="序號"
  width="70">
  <template>
    <el-checkbox> { toggleRowSelection(val, scope.row) }"/>
  /<el-checkbox>/<template>
/<el-table-column>/<code>

checkedRowIds是一個對象,裡面包含了key為id的集合,就像這樣:

<code>const checkedRowIds = {
    0: true,
    1: true,
    2: false
}/<code>

當row.id = 1時,checkedRowIds[row.id] 相當於 checkedRowIds.1 這樣的形式,這個值就是true,為ture就渲染勾選狀態。然後讓我們看下change事件的回調函數toggleRowSelection

<code>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
},/<code>

首先解構拿到checkedRowIds和row.id,然後通過this.$set方法把key為id,值為val的這一項設置到checkedRowIds對象集合裡邊。然後判斷val是否為false且checkedRowIds裡邊存在這個key,滿足兩個條件則刪除這個屬性。最後通過Object.keys()拿到checkedRowIds裡邊所有的key組成的集合,然後通過join()方法組成字符串ids。分析一下:這個方法非常的棒,!!checkedRowIds[scope.row.id]這句話非常短小精悍,非常美。然後在進入頁面默認勾選中非常方便,只要checkedRowIds裡面有的,就會顯示勾選,不用再去調用toggleRowSelection設置。

4.2 第二種利用row-key與reserve-selection的組合拳(晗__童鞋提出)

<code>// 利用row-key與reserve-selection的組合拳
 <el-table>  v-loading="loading"
  :data="tableData"
  row-key="id"
  @selection-change="rowChange"
>

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

/<el-table-column>/<el-table>/<code>

設置了row-key為id後,每一行都有一個不唯一的key值,然後通過reserve-selection字段保留每一次選中的結果,這樣不管怎麼切換頁碼,都可以保留之前用戶選中的值。分析一下:這個辦法非常簡單,而且主要是官方提供的屬性,不用改變太多代碼,還是不錯的。但是在進入頁面後,要顯示默認勾選的row,就需要與tableData中的數據進行對比,然後通過toggleRowSelection方法設置。

寫在後面

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

非常感謝大家的集思廣益,大家在評論區的留言每一條我都仔細看,每一種方案我都會去實踐,感謝大家。希望在這條路上,能夠走的更遠。

作者:彭小呆鏈接:https://juejin.im/post/5e13d9ae5188253aae7d85fa


分享到:


相關文章: