一、背景
在我们开发项目中,经常会遇到需要展示大量选项的多选框场景,比如权限配置、数据筛选等。当选项数量达到几百甚至上千条时,传统的渲染方式全选时会非常卡顿,导致性能问题。本篇文章,记录我使用通过虚拟滚动实现大数据量全选卡顿问题~封装成组件啦可以直接用!
二、效果图
三、功能特点
- 虚拟滚动:只渲染可视区域的选项,大幅提升性能
- 搜索过滤:支持选项实时搜索
- 全选/反选:一键操作所有选项
- 默认选中:支持初始化选中项
- 性能优化:使用节流和防抖处理滚动和搜索
四、组件virtual-checkbox.vue完整代码
<template>
<div class="virtual-checkbox">
<el-input
v-if="showSearch"
v-model="keyword"
prefix-icon="el-input__icon el-icon-search"
type="text"
placeholder="搜索"
@input="seachKey">
</el-input>
<el-checkbox v-model="checkAll" :style="`height:${itemH}px`" class="check-all-box" :indeterminate="isIndeterminate" @change="handleCheckAllChange">
全选
</el-checkbox>
<div ref="scrollBox" :style="`width:${viewW}px;height:${viewH}px;line-height:${itemH}px;overflow-y:auto`" @scroll="handleScroll">
<div :style="`height:${scrollH}px;min-height:${viewH - 22}px`" class="list">
<el-checkbox-group v-if="searchOptions.length" v-model="checkedList" :style="`transform:translateY(${offsetY}px)`" @change="handleCheckChange">
<el-checkbox v-for="item in viewOptions" :key="item.value" :label="item.value" :style="`height:${itemH}px`" @change="handleCheckChange">
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
<div v-else class="empty-text" :style="`height:${viewH - 22}px`">
暂无数据
</div>
</div>
</div>
</div>
</template>
<script>
import { throttle, debounce } from 'lodash'
/**
* @component VirtualCheckbox
* @description 虚拟滚动多选框组件,用于处理大数据量的选项列表。
* 实现了以下功能:
* 1. 虚拟滚动:只渲染可视区域的选项,优化性能
* 2. 搜索过滤:支持选项搜索
* 3. 全选/反选:支持一键全选/反选
* 4. 默认选中:支持默认值回显
*/
export default {
props: {
// 所有选项数据数组,格式:[{label: '选项名', value: '选项值'}]
options: {
type: Array,
default: function () { return [] }
},
// 默认选中项的值数组
defaultChecked: {
type: Array,
default: function () { return [] }
},
// 虚拟列表可视区域高度(像素)
viewH: {
type: Number,
default: function () { return 200 }
},
// 虚拟列表可视区域宽度(像素)
viewW: {
type: Number,
default: function () { return 300 }
},
// 每个选项的高度(像素)
itemH: {
type: Number,
default: function () { return 20 }
},
// 是否显示搜索框
showSearch: {
type: Boolean,
default: true
}
},
data() {
return {
checkAll: false,
isIndeterminate: false,
searchOptions: [], // 搜索后的数据
checkedList: [], // 当前选中的数据
viewOptions: [], // 显示区域的数据
keyword: '', // 搜索关键字
offsetY: 0 // 偏移量
}
},
computed: {
scrollH() {
return this.searchOptions.length * this.itemH
},
// 计算可视区域需要显示的选项数量
visibleCount() {
return Math.floor(this.viewH / this.itemH) + 1
},
// 计算当前显示区域的起始索引
startIndex() {
return Math.floor(this.offsetY / this.itemH)
}
},
watch: {
// 监听默认勾选变化 渲染勾选
defaultChecked: {
handler(val) {
this.checkedList = val
this.handleCheckAllIndeterminate()
},
deep: true,
immediate: true
}
},
beforeDestroy() {
// 清理防抖和节流函数
if (this.throttledScroll) {
this.throttledScroll.cancel()
}
if (this.debouncedSearch) {
this.debouncedSearch.cancel()
}
},
created() {
this.initData()
// 创建节流函数
this.throttledScroll = throttle(this.handleScrollContent, 10)
// 创建防抖函数
this.debouncedSearch = debounce(this.handleSearch, 300)
},
methods: {
/**
* 处理单个选项的选中状态变化
* @emits change - 触发选中数据变化事件
*/
handleCheckChange() {
this.handleCheckAllIndeterminate()
this.$emit('change', this.getCheckedData())
},
/**
* 处理全选/取消全选
* @param {Boolean} val - 是否全选
* @emits change - 触发选中数据变化事件
*/
handleCheckAllChange(val) {
this.checkedList = val ? this.options.map(item => item.value) : []
this.isIndeterminate = false
this.$emit('change', this.getCheckedData())
},
// 处理全选是否选中或者半选
handleCheckAllIndeterminate() {
this.checkAll = this.checkedList.length === this.options.length
this.isIndeterminate = this.checkedList.length > 0 && this.checkedList.length < this.options.length
},
// 滚动事件
handleScroll(e) {
this.throttledScroll(e)
},
handleScrollContent(e) {
let scrollTop = e.target.scrollTop
this.offsetY = scrollTop - scrollTop % this.itemH
this.viewOptions = this.searchOptions.slice(
this.startIndex,
this.startIndex + this.visibleCount
)
},
// 搜索
seachKey() {
this.debouncedSearch()
},
// 搜索具体实现
/**
* 搜索过滤
* @description 支持对选项label的模糊搜索,大小写不敏感
*/
handleSearch() {
if (this.keyword) {
this.searchOptions = this.options.filter(item =>
String(item.label).toLowerCase().includes(this.keyword.toLowerCase())
)
} else {
this.searchOptions = JSON.parse(JSON.stringify(this.options))
}
this.viewOptions = this.searchOptions.slice(0, Math.floor(this.viewH / this.itemH) + 1)
this.initScroll()
},
// 重置滚动
initScroll() {
const scrollBox = this.$refs.scrollBox
if (scrollBox) {
scrollBox.scrollTop = 0 // 将 scrollTop 设置为 0,确保每次弹出时滚动条回到顶部
this.offsetY = 0
}
},
// 初始化数据
initData() {
this.keyword = ''
this.checkAll = false
this.isIndeterminate = false
this.checkedList = [...this.defaultChecked]
this.searchOptions = this.options.length ? JSON.parse(JSON.stringify(this.options)) : []
this.viewOptions = this.searchOptions.slice(0, Math.floor(this.viewH / this.itemH) + 1)
this.initScroll()
this.handleCheckAllIndeterminate()
this.$emit('change', this.getCheckedData())
},
// 重置所有状态
reset() {
this.initData()
},
/**
* 获取当前选中的数据
* @returns {Object} 包含选中项的值数组和完整数据数组
* @returns {Array} checkedValues - 选中项的value数组
* @returns {Array} checkedItems - 选中项的完整数据数组
*/
getCheckedData() {
return {
// 选中项的value数组
checkedValues: this.checkedList,
// 选中项的完整数据数组
checkedItems: this.options.filter(item => this.checkedList.includes(item.value))
}
}
}
}
</script>
<style lang="scss" scoped>
::v-deep .el-checkbox-group {
display: flex;
flex-direction: column;
.el-checkbox {
display: block;
}
}
.check-all-box {
margin-top: 10px;
}
.empty-text {
color: #ccc;
font-size: 12px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
}
</style>
五、使用示例
<template>
<div class="check-box">
<div class="title">
全选案例
</div>
<VirtualCheckbox :options="options" :default-checked="defaultCheckList" :view-h="500" :item-h="30" @change="change"></VirtualCheckbox>
</div>
</template>
<script>
import VirtualCheckbox from './virtual-checkbox.vue'
export default {
components: { VirtualCheckbox },
data() {
return {
defaultCheckList: [], // 默认选中项
checkList: [], // 当前选中项
options: [] // 所有选项
}
},
created() {
this.getOptions()
},
methods: {
getOptions() {
const data = []
for (let i = 1; i < 1000; i++) {
data.push({
value: i,
label: '选项' + i
})
}
this.options = data
this.defaultCheckList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
},
change(val) {
this.checkList = val.checkedValues // 当前选中的id集合
}
}
}
</script>
<style lang="scss" scoped>
.check-box {
border: 1px solid red;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.title {
font-size: 30px;
font-weight: bold;
margin-bottom: 10px;
}
}
</style>
六、 注意事项
- 项目记得下载lodash,组件使用了lodash的防抖节流
- options 数据格式必须符合 {label: string, value: string|number} 的格式
- itemH 需要与实际选项高度一致,否则可能导致滚动计算错误
- 组件销毁时会自动清理节流和防抖函数