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
,不再造车轮。至于虚拟滚动原理,眼见为实,不见为虚。只渲染所见,销毁所不见,定位于其中。
<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
的键名。二次封装加之虚拟列表,带来两个问题:
filterable
等筛选属性失效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,趁此机会封装成库,分享开源供大家使用。也当聊以慰藉,缅怀一代开源人、一代开源库。