主题:element-plus 2.8.7 select options 为[]导致的 bug
先看复现代码(请使用 v2.8.7),你可用在线 playground来操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <script lang="ts" setup> import { ref } from "vue"; const value = ref<string[]>([]); const options = []; </script>
<template> <el-select v-model="value" multiple filterable allow-create default-first-option :reserve-keyword="false" style="width: 240px" > <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" /> </el-select> </template>
|
复现步骤:切换到 2.8.7,打开 console,点击 select,输入一个值,然后选中该值,此时控制台会报错:
1
| aria.ts:71 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'value')
|
接下来我们开始定位问题:
我们是在点击某个 option 时触发的这个报错,因此和 click 事件相关,所以我们应该去源码的<el-option>
组件看看。
从这个报错信息中,最重要的信息就是.value 报错,说明某个变量是 undefined,不小心执行了 undefined.value 导致的异常,所以要去找 xxx.value;
综上,我们来翻看源码,找到 el-option 源码的位置:https://github1s.com/element-plus/element-plus/blob/2.8.7/packages/components/select/src/option.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <template> <li v-show="visible" :id="id" :class="containerKls" role="option" :aria-disabled="isDisabled || undefined" :aria-selected="itemSelected" @mouseenter="hoverItem" @click.stop="selectOptionClick" > <slot> <span>{{ currentLabel }}</span> </slot> </li> </template>
|
很好!原来每个 option 就是一个 li 标签实现的,并且我们发现,li 标签确实有一个@click 事件!
接着来看该函数的定义:
1 2 3 4 5 6 7 8 9
| import { getCurrentInstance } from 'vue'
const vm = getCurrentInstance().proxy as unknown as SelectOptionProxy
function selectOptionClick() { if (!isDisabled.value) { select.handleOptionSelect(vm); } }
|
这里调用 handleOptionSelect 传入了 vm,需要解释一下,getCurrentInstance会获取当前组件实例,.proxy就能拿到当前组件定义的data,还有props。比如一个子组件props有 name age两个字段,那么就能用vm.name和vm.age获取到这两个props的值。所以这里vm就会携带label和value等。
接着看 handleOptionSelect 的定义,我们可以发现,handleOptionSelect 定义在 useSelect.ts 中,参数option就是上面的vm:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const handleOptionSelect = (option) => { if (props.multiple) { const value = ensureArray(props.modelValue ?? []).slice(); const optionIndex = getValueIndex(value, option); if (optionIndex > -1) { value.splice(optionIndex, 1); } else if (props.multipleLimit <= 0 || value.length < props.multipleLimit) { value.push(option.value); } emit(UPDATE_MODEL_EVENT, value); emitChange(value); if (option.created) { handleQueryChange(""); } if (props.filterable && !props.reserveKeyword) { states.inputValue = ""; } } else { emit(UPDATE_MODEL_EVENT, option.value); emitChange(option.value); expanded.value = false; } focus(); if (expanded.value) return; nextTick(() => { scrollToOption(option); }); };
|
这段代码里,我们来找触发option.value的地方。可以看到getValueIndex函数第二个参数传入了option,那就来看getValueIndex函数:
1 2 3 4 5 6 7 8
| const getValueIndex = (arr: any[] = [], option) => { if (!isObject(option?.value)) return arr.indexOf(option.value);
return arr.findIndex((item) => { return isEqual(get(item, props.valueKey), getValueKey(option)); }); };
|
isObject的定义可以在点击这里
1 2
| export const isObject = (val: unknown): val is Record<any, any> => val !== null && typeof val === 'object'
|
所以我们翻译一下getValueIndex的if判断和执行:如果option?.value不是一个对象,我们就去获取option.value。
仔细想想有没有问题?
undefined是对象吗?不是,所以该if判断为true,就会执行undefined.value,所以我们看handleOptionSelect函数使用到的第一个option就找到了问题~
这里的修复方式很简单:如果option是undefined,则不执行下面的代码。而getValueIndex这个函数的功能是获取当前选中项的index,这里既然没有数据,自然应该返回<0的结果 ,所以我们可以返回-1作为函数返回值:
1 2 3 4 5 6 7 8 9
| const getValueIndex = (arr: any[] = [], option) => { if(option === undefined) return -1; if (!isObject(option?.value)) return arr.indexOf(option.value);
return arr.findIndex((item) => { return isEqual(get(item, props.valueKey), getValueKey(option)); }); };
|