这几天开发vue3项目,引用elment中的el-select和el-option进行下拉框的渲染。在此过程中出现了这样的问题(下述代码有所简化):
首先,我通过下述代码对el-option进行数据绑定
<div class="myInfo_type">
<span
@click="handleClick"
>
</span>
</div>
<div class="myInfo_date">
<el-select
v-model="month_selected"
placeholder="筛选时间"
>
<el-option
v-for="item in month_selector"
:key="item.value"
:label="item.title"
:value="item.value"
>
</el-option>
</el-select>
</div>
其次,在setup中对变量 month_selector进行定义,设置初始值并注入至data
setup() {
let month_selected = ref('');
let month_selector = [
{
title: '筛选时间',
value: '0',
}
];
return {
month_selector,
month_selected
};
}
最后,定义handleClick方法更新month_selector的值,以达到动态更新下拉框内容的目的。
methods: {
handleClick(index) {
this.month_selector = [
{title: "筛选时间", value: "0"},
{title: "2021年6月", value: "2021年6月"}
];
}
}
但是发现,点击触发handleClick方法后,下拉框内容并未发生改变。但是用原生的select和option却能动态更新。那么el-option数据不更新的原因是什么呢?于是我找到elment中el-option的源码:
<template>
<li
@mouseenter="hoverItem"
@click.stop="selectOptionClick"
class="el-select-dropdown__item"
v-show="visible"
:class="{
'selected': itemSelected,
'is-disabled': disabled || groupDisabled || limitReached,
'hover': hover
}">
<slot>
<span>{{ currentLabel }}</span>
</slot>
</li>
</template>
<script type="text/babel">
import Emitter from 'element-ui/src/mixins/emitter';
import { getValueByPath, escapeRegexpString } from 'element-ui/src/utils/util';
export default {
mixins: [Emitter],
name: 'ElOption',
componentName: 'ElOption',
inject: ['select'],
props: {
value: {
required: true
},
label: [String, Number],
created: Boolean,
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
index: -1,
groupDisabled: false,
visible: true,
hitState: false,
hover: false
};
},
computed: {
isObject() {
return Object.prototype.toString.call(this.value).toLowerCase() === '[object object]';
},
currentLabel() {
return this.label || (this.isObject ? '' : this.value);
},
currentValue() {
return this.value || this.label || '';
},
itemSelected() {
if (!this.select.multiple) {
return this.isEqual(this.value, this.select.value);
} else {
return this.contains(this.select.value, this.value);
}
},
limitReached() {
if (this.select.multiple) {
return !this.itemSelected &&
(this.select.value || []).length >= this.select.multipleLimit &&
this.select.multipleLimit > 0;
} else {
return false;
}
}
},
watch: {
currentLabel() {
if (!this.created && !this.select.remote) this.dispatch('ElSelect', 'setSelected');
},
value(val, oldVal) {
const { remote, valueKey } = this.select;
if (!this.created && !remote) {
if (valueKey && typeof val === 'object' && typeof oldVal === 'object' && val[valueKey] === oldVal[valueKey]) {
return;
}
this.dispatch('ElSelect', 'setSelected');
}
}
},
methods: {
isEqual(a, b) {
if (!this.isObject) {
return a === b;
} else {
const valueKey = this.select.valueKey;
return getValueByPath(a, valueKey) === getValueByPath(b, valueKey);
}
},
contains(arr = [], target) {
if (!this.isObject) {
return arr && arr.indexOf(target) > -1;
} else {
const valueKey = this.select.valueKey;
return arr && arr.some(item => {
return getValueByPath(item, valueKey) === getValueByPath(target, valueKey);
});
}
},
handleGroupDisabled(val) {
this.groupDisabled = val;
},
hoverItem() {
if (!this.disabled && !this.groupDisabled) {
this.select.hoverIndex = this.select.options.indexOf(this);
}
},
selectOptionClick() {
if (this.disabled !== true && this.groupDisabled !== true) {
this.dispatch('ElSelect', 'handleOptionClick', [this, true]);
}
},
queryChange(query) {
this.visible = new RegExp(escapeRegexpString(query), 'i').test(this.currentLabel) || this.created;
if (!this.visible) {
this.select.filteredOptionsCount--;
}
}
},
created() {
this.select.options.push(this);
this.select.cachedOptions.push(this);
this.select.optionsCount++;
this.select.filteredOptionsCount++;
this.$on('queryChange', this.queryChange);
this.$on('handleGroupDisabled', this.handleGroupDisabled);
},
beforeDestroy() {
const { selected, multiple } = this.select;
let selectedOptions = multiple ? selected : [selected];
let index = this.select.cachedOptions.indexOf(this);
let selectedIndex = selectedOptions.indexOf(this);
// if option is not selected, remove it from cache
if (index > -1 && selectedIndex < 0) {
this.select.cachedOptions.splice(index, 1);
}
this.select.onOptionDestroy(this.select.options.indexOf(this));
}
};
</script>
我们可以看出来,el-option中展示的内容其实是计算属性currentLabel,而currentLabel是通过监听组件的label和value属性,而label和value绑定的分别是month_selector每项的title和value字段。
那么我推测,虽然month_selector发生改变,但因为其没有响应式的特性,所以并不会引发label和value的监听。因此,我通过ref关键字将month_selector定义为响应式变量,测试通过。
let month_selector = ref([
{
title: '筛选时间',
value: '0',
}
]);
也许会有人问,对象变量不应该是用reactive赋予响应式特性吗?这就需要清楚ref和reactive的区别:
reactive虽然是将对象赋予响应式特性,但由于我的handleClick方法中是直接给month_selector赋值,这样就会丢失响应式的特性。因此大部分情况下还是建议用ref附加响应式特性,reactive操作不当容易丢失响应式特性。
ref是将原始数据类型(字符串、数字等等)赋予响应式特性,但是查看ref的源码(如下),由此可见ref也可以为对象赋予响应式特性。
当然,以上问题除了增加响应式特性之外,也可以通过其他方式解决,比如:
(方法一)给el-option的父级元素(即el-select),定义key属性。当el-option所绑定的数据发生变化的同时,改变父级元素的key属性,这样就会重新渲染父级元素,同时就会重新渲染el-option;
(方法二)将el-option的for循环再封装为新的组件,这样绑定数据发生变化,就会重新渲染该组件。