foreword
In our daily project development, we often have 表格跨页多选
requirements. Next, let us use el-table
examples to realize this requirement step by step.
hands-on development
online experience
https://codesandbox.io/s/priceless-mcclintock-4cp7x3?file=/src/App.vue
regular version
This part only writes some key codes, impatient Yanzu can directly read性能进阶版
- First we need to initialize a selected array
checkedRows
this.checkedRows = []
- When triggering the selection, we need to get the data of the current row
push
,checkedRows
otherwise we need to delete the corresponding row
<el-table ref="multipleTable" @select="handleSelectChange">
handleSelectChange (val, row) {
const checkedIndex = this.checkedRows.findIndex(_row => _row.id === row.id)
if (checkedIndex > -1) {
// 选中剔除
this.checkedRows.splice(checkedIndex, 1)
} else {
// 未选中压入
this.checkedRows.push(row)
}
}
- Echo logic when implementing page change
this.data.forEach(row=>{
const checkedIndex = this.checkedRows.findIndex(_row => _row.id === row.id)
if(checkedIndex>-1) this.$refs.multipleTable.toggleRowSelection(row,true)
})
Effect preview
Let's see the effect at this time
full code
<template>
<div>
<el-table
ref="multipleTable"
:data="tableData"
tooltip-effect="dark"
style="width: 100%"
@select="handleSelectChange"
@select-all="handleSelectAllChange"
>
<el-table-column
type="selection"
width="55"
/>
<el-table-column
label="日期"
width="120"
prop="date"
/>
<el-table-column
prop="name"
label="姓名"
width="120"
/>
</el-table>
<el-pagination
background
:current-page.sync="currentPage"
layout="prev, pager, next"
:total="1000"
@current-change="currentChange"
/>
</div>
</template>
<script>
export default {
data () {
return {
currentPage: 1,
checkedRows: [],
pageSize: 10,
totalData: Array.from({
length: 1000 }, (_, index) => {
return {
date: '2016-05-03',
id: index,
name: '王小虎' + index
}
})
}
},
computed: {
tableData () {
const {
currentPage, totalData, pageSize } = this
return totalData.slice((currentPage - 1) * pageSize, currentPage * pageSize)
}
},
methods: {
currentChange (page) {
this.currentPage = page
this.tableData.forEach(row => {
const checkedIndex = this.checkedRows.findIndex(_row => _row.id === row.id)
if (checkedIndex > -1) this.$refs.multipleTable.toggleRowSelection(row, true)
})
},
handleSelectChange (val, row) {
const checkedIndex = this.checkedRows.findIndex(_row => _row.id === row.id)
if (checkedIndex > -1) {
this.checkedRows.splice(checkedIndex, 1)
} else {
this.checkedRows.push(row)
}
},
handleSelectAllChange (val) {
this.tableData.forEach(row => {
this.handleSelectChange(null, row)
})
}
}
}
</script>
Performance Advanced Edition
performance defect analysis
Excellent Yan ancestors, you should find the performance defects of the above code
1. handleSelectChange
Need to execute a O(n)
complexity cycle
2. currentChange
Inside the echo logic, there is a O(n^2)
complexity loop
Imagine if the number of checked lines in the scene reaches 10000
lines, each page displays 100
items
Then we have to execute worst- 10000 * 100
case loops every time we click to change pages, which is a terrible thing...
Redesign the data structure
In fact, we don't need to checkedRows
design it as an array, we can design it as one map
, so that reading the value only requires O(1)
complexity
1. TransformationcheckedRows
this.crossPageMap = new Map()
2. Modify the selection logic ( 核心代码
)
handleSelectChange (val, row) {
// 实现了 O(n) 到 O(1) 的提升
const checked = this.crossPageMap.has(row.id)
if (checked) {
this.crossPageMap.delete(row.id)
} else {
this.crossPageMap.set(row.id, row)
}
}
3. Modify the page change echo logic
currentChange (page) {
this.currentPage = page
// 实现了 O(n^2) 到 O(n) 的提升
this.tableData.forEach(row => {
const checked = this.crossPageMap.has(row.id)
if (checked) this.$refs.multipleTable.toggleRowSelection(row, true)
})
}
full code
<template>
<div>
<el-table
ref="multipleTable"
:data="tableData"
tooltip-effect="dark"
style="width: 100%;height:500px"
@select="handleSelectChange"
@select-all="handleSelectAllChange"
>
<el-table-column
type="selection"
width="55"
/>
<el-table-column
label="日期"
width="120"
prop="date"
/>
<el-table-column
prop="name"
label="姓名"
width="120"
/>
</el-table>
<el-pagination
background
:current-page.sync="currentPage"
layout="prev, pager, next"
:total="1000"
@current-change="currentChange"
/>
</div>
</template>
<script>
export default {
data () {
return {
currentPage: 1,
crossPageMap: new Map(),
pageSize: 10,
totalData: Array.from({
length: 1000 }, (_, index) => {
return {
date: '2016-05-03',
id: index,
name: '王小虎' + index
}
})
}
},
computed: {
tableData () {
const {
currentPage, totalData, pageSize } = this
return totalData.slice((currentPage - 1) * pageSize, currentPage * pageSize)
}
},
methods: {
currentChange (page) {
this.currentPage = page
this.tableData.forEach(row => {
const checked = this.crossPageMap.has(row.id)
if (checked) this.$refs.multipleTable.toggleRowSelection(row, true)
})
},
handleSelectChange (val, row) {
const checked = this.crossPageMap.has(row.id)
if (checked) {
this.crossPageMap.delete(row.id)
} else {
this.crossPageMap.set(row.id, row)
}
},
handleSelectAllChange (val) {
this.tableData.forEach(row => {
this.handleSelectChange(null, row)
})
}
}
}
</script>
abstract business logic
The above is the complete business code part, but for reusability.
We consider that the logic can be abstracted into a CrossPage
class
Design the CrossPage class
Receive the following parameters
`data` - 行数据
`key` - 行数据唯一值
`max` - 最大选中行数
`toggleRowSelection` - 切换行数据选中/取消选中的方法
Provides the following methods
`onRowSelectChange` - 外部点行数据点击的时候调用此方法
`onDataChange` - 外部数据变化的时候调用此方法
`clear` - 清空所有选中行
The general code of the constructor is as follows
constructor (options={
}) {
this.crossPageMap = new Map()
this.key = options.key || 'id'
this.data = options.data || []
this.max = options.max || Number.MAX_SAFE_INTEGER
this.toggleRowSelection = options.toggleRowSelection
if(typeof this.toggleRowSelection !== 'function') throw new Error('toggleRowSelection is not function')
}
Set private crossPageMap
Yan ancestors, the problem is coming, we mount it crossPageMap
on the instance, then the outside can directly access and modify this variable.
This may lead to confusion in our internal data logic, so external access must be prohibited.
We can use #
modifiers to implement private properties, see for details
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes/Private_class_fields
full code
- CrossPage.js
/**
* @description 跨页选择
* @param {Object} options
* @param {String} options.key 行数据唯一标识
* @param {Array} options.data 行数据
* @param {Number} options.max 最大勾选行数
* @param {Function} options.toggleRowSelection 设置行数据选中/取消选中的方法,必传
*/
export const CrossPage = class {
#crossPageMap = new Map();
constructor (options={
}) {
this.key = options.key || 'id'
this.data = options.data || []
this.max = options.max || Number.MAX_SAFE_INTEGER
this.toggleRowSelection = options.toggleRowSelection
if(typeof this.toggleRowSelection !== 'function') throw new Error('toggleRowSelection is not function')
}
get keys(){
return Array.from(this.#crossPageMap.keys())
}
get values(){
return Array.from(this.#crossPageMap.values())
}
get size(){
return this.#crossPageMap.size
}
clear(){
this.#crossPageMap.clear()
this.updateViews()
}
onRowSelectChange (row) {
if(typeof row !== 'object') return console.error('row is not object')
const {
key,toggleRowSelection} = this
const checked = this.#crossPageMap.has(row[key])
if(checked) this.#crossPageMap.delete(row[key])
else {
this.#crossPageMap.set(row[key],row)
if(this.size>this.max){
this.#crossPageMap.delete(row[key])
toggleRowSelection(row,false)
}
}
}
onDataChange(list){
this.data = list
this.updateViews()
}
updateViews(){
const {
data,toggleRowSelection,key} = this
data.forEach(row=>{
toggleRowSelection(row,this.#crossPageMap.has(row[key]))
})
}
}
- crossPage.vue
<template>
<div>
<el-table
ref="multipleTable"
:data="tableData"
tooltip-effect="dark"
style="width: 100%"
@select="handleSelectChange"
@select-all="handleSelectAllChange"
>
<el-table-column
type="selection"
width="55"
/>
<el-table-column
label="日期"
width="120"
prop="date"
/>
<el-table-column
prop="name"
label="姓名"
width="120"
/>
</el-table>
<el-button @click="clear">
清空
</el-button>
<el-button @click="keys">
获取 keys
</el-button>
<el-button @click="values">
获取 values
</el-button>
<el-pagination
background
:current-page.sync="currentPage"
layout="prev, pager, next"
:total="1000"
@current-change="currentChange"
/>
</div>
</template>
<script>
import {
CrossPage } from './CrossPage'
export default {
data () {
return {
currentPage: 1,
pageSize: 10,
totalData: Array.from({
length: 1000 }, (_, index) => {
return {
date: '2016-05-03',
id: index,
name: '王小虎' + index
}
}),
multipleSelection: []
}
},
computed: {
tableData () {
const {
currentPage, totalData, pageSize } = this
return totalData.slice((currentPage - 1) * pageSize, currentPage * pageSize)
}
},
mounted () {
this.crossPageIns = new CrossPage({
key: 'id',
max: 2,
data: this.tableData,
toggleRowSelection: this.$refs.multipleTable.toggleRowSelection
})
},
methods: {
clear () {
this.crossPageIns.clear()
},
keys () {
console.log('keys:', this.crossPageIns.keys)
},
values () {
console.log('values:', this.crossPageIns.values)
},
currentChange (page) {
this.currentPage = page
// 调用实例 onDataChange 方法
this.crossPageIns.onDataChange(this.tableData)
},
handleSelectChange (val, row) {
// 调用实例 onRowSelectChange 方法
this.crossPageIns.onRowSelectChange(row)
},
handleSelectAllChange (val) {
this.tableData.forEach(row => {
this.crossPageIns.onRowSelectChange(row)
})
}
}
}
</script>
write at the end
There are still many things I want to do in the future
- Use to improve the rendering efficiency
requestIdleCallback
of a large amount of data on a single pagetoggleRowSelection
- Provide configuration for default selected items
- …