解讀element-ui中table元件部分原始碼與需求分析

彭小呆發表於2020-01-07

一、前言

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元件中加一個typeselection的列就行了。

<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中table元件部分原始碼與需求分析

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

為什麼這麼說?因為在公司的實際業務中,表格是分頁表格,每次切換頁碼,資料重新獲取,表格重新渲染,那麼第一個問題來了:使用者在頁碼為1的表格選中的行,在切換頁碼之後,不見了。

分頁表格應該像下面這樣:

解讀element-ui中table元件部分原始碼與需求分析

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

解讀element-ui中table元件部分原始碼與需求分析

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

坑又馬上來了!!

因為通過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元件中是如何判斷選中的?

三、原始碼分析

僅僅是table元件部分原始碼。

3.1 結構

首先看下table元件的結構

解讀element-ui中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吧。

解讀element-ui中table元件部分原始碼與需求分析

得到了78個結果,在7個檔案中。得到的結果太多了,我們不想要這樣的結果。

然後進一步用全匹配搜尋:

解讀element-ui中table元件部分原始碼與需求分析

得到了38個結果,在5個檔案中。

縮小了一些範圍,但是還是很多,也沒辦法了。一個一個檔案找。

純按照Vscode給我搜出來的順序,第一個檔案是config.js檔案。

解讀element-ui中table元件部分原始碼與需求分析

這個關鍵詞在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中,很明顯能找到它:

解讀element-ui中table元件部分原始碼與需求分析

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

解讀element-ui中table元件部分原始碼與需求分析

然後這兩個檔案不用管了,因為我們找到了佈局的位置,回到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中,我們找到了它:

解讀element-ui中table元件部分原始碼與需求分析

// 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他喵的不相等。

解讀element-ui中table元件部分原始碼與需求分析

但是根據我們的實際業務,row1和row2結構一樣,id一樣,這兩個玩意就是一個東西。

舉個更實際的例子:你二舅在村裡的瓦房裡出來,你認出來了是你二舅;你在北京東二環的某個小區裡看見你二舅從某個單元出來,你二舅他喵的不是你的二舅了。這太扯了!!

所以,意思就是說在table元件中原始碼渲染的時候的判斷,太簡單了,沒有更深的判斷,只比較引用地址是否相同。

找到問題的根本所在,解決起來也是相當的容易。

四、解決方案

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

以上辦法,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是一個物件,裡面包含了keyid的集合,就像這樣:

const checkedRowIds = {
    0: true,
    1: true,
    2: false
}
複製程式碼

當row.id = 1時,checkedRowIds[row.id] 相當於 checkedRowIds.1 這樣的形式,這個值就是true,為ture就渲染勾選狀態。

然後讓我們看下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方法把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的組合拳(晗__童鞋提出)

// 利用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>
複製程式碼

設定了row-key為id後,每一行都有一個不唯一的key值,然後通過reserve-selection欄位保留每一次選中的結果,這樣不管怎麼切換頁碼,都可以保留之前使用者選中的值。

分析一下:這個辦法非常簡單,而且主要是官方提供的屬性,不用改變太多程式碼,還是不錯的。但是在進入頁面後,要顯示預設勾選的row,就需要與tableData中的資料進行對比,然後通過toggleRowSelection方法設定。

五、寫在後面

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

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

相關文章