一个基于 Element UI 的 Select 虚拟列表选择器

Select V2 虚拟列表选择器

前言

万条数据的列表选择器,若一次全部渲染则性能不佳。在 Vue 3 中,可使用 Element Plus 的 Select V2 虚拟列表选择器。而 Vue 2 的 Element UI 则无此功能。我在 GitHub 搜寻很久,没有找到一款功能齐全合适的 Element UI 虚拟列表选择器。索性自己动手,丰衣足食。加之本人对于 Element 系组件的熟悉程度,二次封装应能满足。

项目展示

既基于 Element UI 的 Select 二次封装,则功能越接近、使用越相似为好。可见 Demo 中的用法,几乎复制了 Element UI 的 Select 使用文档。同时参考了 Element Plus 中 Select V2 组件,易于使用。

开发流程

考虑到二次开发,不改、少改的原则。el-select 与 el-option 组件需要保留,属性、方法、事件与插槽也同理。虚拟滚动列表已有优秀开源组件,此处选用 vue-virtual-scroller,不再造车轮。至于虚拟滚动原理,眼见为实,不见为虚。只渲染所见,销毁所不见,定位于其中。

vue 复制代码
<template>
  <el-select
    ref="select"
    v-model="localValue"
    // 此处省略若干属性,详见于源码
    :filter-method="filterMethod || localFilterMethod"
    v-bind="$attrs"
    v-on="$listeners"
    @focus="handleSelectFocus"
  >
    <RecycleScroller
      v-if="localOptions.length"
      ref="scroller"
      v-slot="{ item }"
      class="scroller"
      :items="localOptions"
      :min-item-size="minItemSize"
      :key-field="valueKey"
      @visible="handleScrollerVisible"
    >
      <el-option
        :key="item[valueKey]"
        :value="item[valueKey]"
        :label="item[labelKey]"
        :disabled="item.disabled"
      >
        <slot name="default" :item="item" />
      </el-option>
    </RecycleScroller>
    <template v-if="$slots.prefix" slot="prefix">
      <slot name="prefix" />
    </template>
    <template v-if="$slots.empty" slot="empty">
      <slot name="empty" />
    </template>
  </el-select>
</template>

上述 template 代码,可见保留了原 el-select 的用法。el-option 由默认插槽改为 options 数组。options 为数组对象,内部参数与 el-option 类似,包含 value、label、disabled。用户可传入 labelKey 与 valueKey 设置 label 与 value 的键名。二次封装加之虚拟列表,带来两个问题:

  1. filterable 等筛选属性失效
  2. value 初次加载或更新,label 值失效

第一问,原 el-select 为内部控制 el-option 筛选,封装组件传入 options 属性导致内部筛选失效,需封装组件控制。可利用 filterMethod 属性,实现 localFilterMethod 方法,如下:

js 复制代码
localFilterMethod(query) {
  this.localOptions = this.options.filter(option => option[this.labelKey].toLowerCase().includes(query.toLowerCase()));
},

设置 localOptions 属性,用于存放筛选后的选项。将 options 作为原始值保存,组件内部使用 localOptions。注意留出 filterMethod 属性,可供用户自定义筛选方法。

第二问,原 el-select 获取 label 为遍历查找 el-option。由于二次封装的虚拟列表只渲染所见,仅渲染的部分 el-option 不足以匹配到。

在 GitHub 发现有项目是如此实现的:

当 el-select 下拉菜单收起时,使用 v-if 渲染一份不含虚拟列表包裹的 el-option,循环数组为当前选中的值。下拉菜单展开时,再切换至虚拟列表。此方法着实有效,使用虚拟列表与真实列表巧妙切换,在体感上无缝衔接。然而缺点是代码冗余,不易维护。

我试图从 el-select 源码中寻找最佳答案。在搜寻中,我发现了 cachedOptions 属性,用于存放选项的缓存,当前 label 即从缓存中读取。既如此,手动向 cachedOptions 中添加缓存也不失为好办法。当 value 值变化时,el-select 会触发 setSelected 方法,用于更新当前 label。所述封装为 updateSelectedLabel 方法,如下:

js 复制代码
updateSelectedLabel() {
  if (!this.$refs.select) {
    return;
  }
  const { setSelected, cachedOptions } = this.$refs.select;
  const values = this.multiple ? this.localValue : [this.localValue];
  const selectedOptions = this.options.filter(option => values?.includes(option[this.valueKey])).map(option => ({
    value: option[this.valueKey],
    currentLabel: option[this.labelKey],
  }));
  selectedOptions.forEach(option => {
    const cachedOption = cachedOptions.find(cachedOption => cachedOption.value === option.value);
    if (cachedOption) {
      cachedOption.currentLabel = option.currentLabel;
    } else {
      cachedOptions.push(option);
    }
  });
  setSelected();
},

在组件初始化时与 value 改变时触发该方法更新 label。

除此之外,还有个别小问题:

  • Q1:筛选后再次展开下拉菜单,还是筛选后的结果
  • A1:每次 focus 时,重新赋值 localOptions
  • Q2:选择值后展开下拉菜单,滚动条不会定位到当前选中位置
  • A2:使用 RecycleScroller 组件的 scrollToItem 方法定位
  • Q3:滚动条拖动异常,可能点击不了
  • A3:二次封装后,需给原滚动条 el-scrollbar__bar 设置 display: none 隐藏

总结

由于种种原因,Element UI 已停滞更新。Element Plus 中新特性与功能也未可反哺。因工作仍需使用 Vue 2 与 Element UI,趁此机会封装成库,分享开源供大家使用。也当聊以慰藉,缅怀一代开源人、一代开源库。

项目地址

项目源码:github.com/kooriookami...

在线演示:kooriookami.github.io/el-select-v...

相关推荐
橙光成长4 小时前
PDF离线工具箱深度解析:4MB纯前端单文件,40+功能全本地运行,文件绝不上传
前端·pdf·1024程序员节
小poop10 小时前
CSS 基础与实战:从入门到精通网页美化
前端
Csvn12 小时前
运行时性能
前端
Never_every9912 小时前
位图转矢量工具哪个好?实测抠抠图AI位图转矢量工具
大数据·前端
paopaokaka_luck13 小时前
宠物门店经营管理系统(销售与库存联动、进货自动记账、 退换与退款关联、员工个人业绩、ECharts 经营分析、滞销识别与客户分层、宠物档案年龄计算)
前端·javascript·spring boot·mysql·echarts·mybatis·1024程序员节
空中湖13 小时前
用 React 写数据大屏:为什么我不建议用图表库
前端·react.js·前端框架
欣欣之王来了15 小时前
原始HTML插值:v-html指令的使用与安全风险
前端·学习·架构·项目·vue3教程
IT_陈寒15 小时前
Redis的LRU淘汰策略坑了我一整天
前端·人工智能·后端
计算机魔术师15 小时前
砍半价格、参数再涨,国产旗舰追到 Opus 鼻尖了
前端
paopaokaka_luck15 小时前
心理咨询管理系统(WebSocket实时沟通、基于用户的协同过滤、 ECharts多维分析、 预约业务流程、预约校验与定时处理)
java·vue.js·spring boot·mysql·spring·echarts·1024程序员节