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

2,805 阅读3分钟

Select V2 虚拟列表选择器

前言

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

项目展示

image.png

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

开发流程

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

<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 类似,包含 valuelabeldisabled。用户可传入 labelKeyvalueKey 设置 labelvalue 的键名。二次封装加之虚拟列表,带来两个问题:

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

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

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 方法,如下:

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…