Vue el-checkbox 虚拟滚动解决多选框全选卡顿问题 - 高性能处理大数据量选项列表

一、背景

在我们开发项目中,经常会遇到需要展示大量选项的多选框场景,比如权限配置、数据筛选等。当选项数量达到几百甚至上千条时,传统的渲染方式全选时会非常卡顿,导致性能问题。本篇文章,记录我使用通过虚拟滚动实现大数据量全选卡顿问题~封装成组件啦可以直接用!

二、效果图

在这里插入图片描述

三、功能特点

  • 虚拟滚动:只渲染可视区域的选项,大幅提升性能
  • 搜索过滤:支持选项实时搜索
  • 全选/反选:一键操作所有选项
  • 默认选中:支持初始化选中项
  • 性能优化:使用节流和防抖处理滚动和搜索

四、组件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>

六、 注意事项

  1. 项目记得下载lodash,组件使用了lodash的防抖节流
  2. options 数据格式必须符合 {label: string, value: string|number} 的格式
  3. itemH 需要与实际选项高度一致,否则可能导致滚动计算错误
  4. 组件销毁时会自动清理节流和防抖函数

有用的话就点个赞吧~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值