<template>
<div>
<el-select v-model="obj.users" multiple>
<el-option value="a" label="a"></el-option>
<el-option value="b" label="b"></el-option>
<el-option value="c" label="c"></el-option>
</el-select>
<div>选择结果{{ obj }}</div>
<el-button @click="setUsers1">set users1</el-button>
<el-button @click="setUsers2">set users2</el-button>
</div>
</template>
<script>
export default {
data() {
return {
obj: {},
}
},
methods: {
setUsers1() {
this.obj = {}
this.obj.users = undefined // 写法1
// this.$set(this.obj, 'users', []) // 写法2
},
setUsers2() {
this.$set(this.obj, 'users', [])
// this.$forceUpdate() // 333
},
},
}
</script>
复现步骤
- 点击下拉选择, 几个选项
- 点击
set users1重新赋值 obj, 为空对象, 再给 obj 一个 users 属性, 值为 undefined - 点击
set users2给 obj 的 users 属性设置为 空数组 - 再次在下拉框中选择
期望
选择结果显示出来
实际
下拉选择之后没反应, 选择结果还是空的
原因
setUsers1 中给 obj 赋值 users 属性时, 没有使用 this.$set, 导致 vue 没有给 obj 的 users 属性设置响应. 在这种情况下, 调用 setUsers2 中的 this.$set 时, vue 内部判断 obj 上已经有 users 属性, 认为已经进行了响应式监听, 就直接给这个属性赋值, 没有触发更新的广播
Vue.$set 源码
export function set (target: Array<any> | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val // 这里, 如果对象上已经有这个属性了, 赋值完直接返回, 就不会走到下面依赖广播的逻辑了
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
如何修正
- 方法1: setUsers1 中,
写法1改成写法2或直接写成this.obj = { users: undefined } - 方法2: setUsers2 中打开注释
333
最佳实践
给 vue 实例的属性对象上添加属性时, 不要直接赋值, 要用 Vue.$set 或 this.$set