场景描述
在使用 el-transfer
组件时,当左侧列表数据过多,例如下面这个场景需要加载 8000 多条数据时,页面滚动会卡,复选框点击后延迟几秒才能选中...这样体验很差。
优化方案
我想到的预选方案有两种:
-
虚拟列表
-
滚动加载(原理是模拟分页,当滚动到底部时,加载下一页)
现在分析一下以上这两种方案的优缺点。
虚拟列表
优点:
- 无论多少数据页面滚动无压力
- 滚动条的长度能代表数据量的多少(滚动条越短,数据量越多)。
缺点:
- 需要改造
el-transfer
组件源码,增加代码量 - 改造逻辑复杂
- 已选的条目有可能不在
dom
中,上下滚动过程要做回显处理 - 改变搜索条件时,需要回显已选条目
滚动加载
优点:
- 不用改造
el-transfer
组件源码,代码量少 - 逻辑简单明了
- 已选条目在
dom
中,上下滚动过程不需要做回显处理
缺点:
- 滚动条长度不能代表数据量,随着向下滚动,滚动条逐渐变短
- 当一直向下滚动时,列表加载过多的条目,页面一样会卡(但是很少有人向下滚动上千条吧..)
综上分析,我更倾向滚动加载。以下是滚动加载的实现方案,而且是前端分页,所有的数据已经一次性查询回来。
代码实现
<template>
<div id="transfer-form">
<el-transfer
ref="myTransfer"
:titles="['全选', '全选']"
:props="{
key: 'key',
label: 'name'
}"
filterable
:filter-method="filterMethod"
filter-placeholder="请输入需要检索的表名"
v-model="selectedData"
:data="tableList">
</el-transfer>
</div>
</template>
<script>
import { debounce } from 'lodash'
export default {
name: 'BatchMapping',
data () {
return {
tableList: [],
scrollList: [],
scrollFilterList: [],
scrollPageSize: 20,
scrollPageNum: 0,
scrollQuery: ''
}
},
mounted () {
const me = this
// 自定义id方便查找dom
const transferForm = document.getElementById('transfer-form')
// 找到滚动容器dom
const transferPanelList = transferForm.querySelector('.el-transfer-panel__list')
// 添加scroll事件
transferPanelList && transferPanelList.addEventListener('scroll', function () {
// 滚动到底部
if (transferPanelList.scrollTop + transferPanelList.clientHeight >= transferPanelList.scrollHeight) {
me.scrollLoadList()
}
})
},
methods: {
// 筛选方法永远返回true
filterMethod (query, item) {
return true
},
effectModel () {
// 从接口获取列表数据
_req.getDataSourceTableList(params).then(res => {
if (res.success) {
this.initScrollLoad(list)
}
})
}
},
// 初始化滚动加载
initScrollLoad (list) {
// 如果有取消监听函数,要执行。防止重复监听
this.scrollQueryWatch && this.scrollQueryWatch()
const me = this
// 保存所有的数据到本地
this.scrollList = Object.freeze(list)
this.scrollQuery = ''
// 清空el-transfer组件中leftPanel子组件中的query属性
this.$refs.myTransfer.$refs.leftPanel.query = ''
// 监听el-transfer组件中leftPanel子组件中的query属性
// 保存取消监听函数
this.scrollQueryWatch = this.$refs.myTransfer.$refs.leftPanel.$watch('query', function (val) {
// 保存query
me.scrollQuery = val
me.scrollDebounceQuery()
})
this.scrollFilterList = this.scrollList
this.scrollPageNum = 1
this.tableList = []
this.scrollLoadList()
},
// 滚动加载
scrollLoadList () {
const startIndex = this.scrollPageSize * (this.scrollPageNum - 1) // 计算开始位置
const endIndex = startIndex + this.scrollPageSize // 计算结束位置
// 如果开始位置已经超过总数据量 直接返回
if (startIndex >= this.scrollFilterList.length) return
const subList = this.scrollFilterList.slice(startIndex, endIndex)
this.tableList.push(...subList)
this.scrollPageNum += 1
},
// 防抖查询
scrollDebounceQuery: debounce(function () {
this.scrollPageNum = 1
// 默认把选中的数据放入tableList
this.tableList = this.scrollList.filter(item => this.selectedData.includes(item.key))
// 过滤出符合条件的数据,注意这里要排除已选中的
this.scrollFilterList = this.scrollList.filter(item => {
return (item.name.toLowerCase().indexOf(this.scrollQuery.toLowerCase()) !== -1) && !this.selectedData.includes(item.key)
})
this.scrollLoadList()
}, 1000),
},
}
</script>
提到 mixins
<template>
<el-form-item label="提取模型" prop="tableList" class="transfer-from-item" id="transfer-form">
<el-transfer
ref="myTransfer"
:titles="['全选', '全选']"
:props="{
key: 'key',
label: 'name'
}"
filterable
:filter-method="scrollTransferFilterMethod"
filter-placeholder="请输入需要检索的表名"
v-model="selectedData"
:data="form.tableList">
</el-transfer>
</el-form-item>
</template>
<script>
import transferScrollMixin from '@/utils/transferScrollMixin'
export default {
name: 'BatchMapping',
data () {
return {
tableList: []
}
},
methods: {
effectModel () {
// 从接口获取列表数据
_req.getDataSourceTableList(params).then(res => {
if (res.success) {
this.initScrollLoad(list)
}
})
}
}
}
</script>
transferScrollMixin.js
import { debounce } from 'lodash'
export default {
data () {
return {
selectedData: [], // 选中行
scrollList: [],
scrollFilterList: [],
scrollPageSize: 20,
scrollPageNum: 1,
scrollQuery: '',
scrollQueryWatch: null
}
},
mounted () {
const me = this
/* el-transfer 的包裹元素,需要自定义 */
const transferContainer = document.getElementById('transfer-form')
const transferPanelList = transferContainer.querySelector('.el-transfer-panel__list')
transferPanelList && transferPanelList.addEventListener('scroll', function () {
if (transferPanelList.scrollTop + transferPanelList.clientHeight >= transferPanelList.scrollHeight) {
me.scrollLoadList()
}
})
},
methods: {
getTransferVm () {
/* 自定义 el-transfer 的 ref 值 */
return this.$refs.myTransfer
},
setTransferData (list = [], isPush) {
if (isPush) {
this.tableList.push(...list) // 自定义 el-transfer data 属性绑定的属性
} else {
this.tableList = list // 自定义 el-transfer data 属性绑定的属性
}
},
scrollTransferFilterMethod (query, item) {
return true
},
initScrollLoad (list) {
this.scrollQueryWatch && this.scrollQueryWatch()
const me = this
this.scrollList = Object.freeze(list)
this.scrollQuery = ''
const transferVm = this.getTransferVm()
transferVm.$refs.leftPanel.query = ''
// 监听左侧query变化
this.scrollQueryWatch = transferVm.$refs.leftPanel.$watch('query', function (val) {
me.scrollQuery = val
me.scrollDebounceQuery()
})
this.scrollFilterList = this.scrollList
this.scrollPageNum = 1
this.setTransferData()
this.scrollLoadList()
},
scrollLoadList () {
const startIndex = this.scrollPageSize * (this.scrollPageNum - 1)
const endIndex = startIndex + this.scrollPageSize
if (startIndex >= this.scrollFilterList.length) return
const subList = this.scrollFilterList.slice(startIndex, endIndex)
this.setTransferData(subList, true)
this.scrollPageNum += 1
},
scrollDebounceQuery: debounce(function () {
this.scrollPageNum = 1
this.setTransferData(this.scrollList.filter(item => this.selectedData.includes(item.key)))
this.scrollFilterList = this.scrollList.filter(item => {
return (item.name.toLowerCase().indexOf(this.scrollQuery.toLowerCase()) !== -1) && !this.selectedData.includes(item.key)
})
this.scrollLoadList()
}, 1000)
}
}