在vue开发中,经常会遇到这样的问题:当我们给响应式的对象新增属性时,新增的属性并不会显示到页面中;同样的对于响应式的数组,增加元素、修改数组长度时,数组的这些变化也不会反映到页面中(响应式对象和响应式数组是指在vue初始化时期,利用Object.defineProperty()方法对其进行监听,这样在修改数据时会及时体现在页面上)。如下所示:
<div id="app">
{{ obj }}
{{ arr }}
div>
let vm = new Vue({
el: '#app',
data() {
return {
obj: {
a: '哈哈哈',
},
arr: [1, 3]
}
},
mounted() {
this.changeObj()
this.changeArr()
},
methods: {
changeObj() {
this.obj.b = '新增的属性b'
console.log(this.obj)
},
changeArr() {
this.arr[4] = 7
this.arr.length = 5
console.log(this.arr)
}
}
})
vm.$data.obj.c = '新增属性c'
vm.$data.arr[2] = 5
从上面例子中可以看出在页面加载后,我们通过this.obj.b
新增属性b时,页面中并没有将属性b显示出来;通过vm.$data.obj.c
新增属性c时,页面中也没有将属性c显示出来;但是通过打印对象obj,可以看出obj上存在属性b和c,为什么呢?
这是由于对象obj及其属性a在vue初始化时已处理成响应式的,即当我们改变对象obj的值或属性a的值时,会触发其在页面上的更新,但是当页面加载完成后,新增的属性b和c就不是响应式的,虽然通过打印this.obj可以看出对象obj上确实增加了属性b和c,但是由于b和c不是响应式的,所以新增的属性不会体现在页面上。
对于数组arr也存在类似的问题,当我们通过索引值添加元素或更改数组长度时,数组本身是发生变化了,但是没有及时体现在页面上。
对于上述问题,我们通常使用vue中的this.$set()
来解决,那么this.$set()
的原理是什么呢?
在使用this.$set(target, key, value)
时,target为需要添加属性的对象,key是要添加的属性名,value为属性key对应的值。
点击查看vue中set源码
// src/core/observer/index.js
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、如果是在开发环境,且target未定义(为null、undefined)或target为基础数据类型(string、boolean、number、symbol)时,抛出告警;
2、如果target为数组且key为有效的数组key时,将数组的长度设置为target.length和key中的最大的那一个,然后调用数组的splice方法(vue中重写的splice方法)添加元素;
3、如果属性key存在于target对象中且key不是Object.prototype上的属性时,表明这是在修改target对象属性key的值(不管target对象是否是响应式的,只要key存在于target对象中,就执行这一步逻辑),此时就直接将value直接赋值给target[key];
4、判断target,当target为vue实例或根数据data对象时,在开发环境下抛错;
5、当一个数据为响应式时,vue会给该数据添加一个__ob__
属性,因此可以通过判断target对象是否存在__ob__
属性来判断target是否是响应式数据,当target是非响应式数据时,我们就按照普通对象添加属性的方式来处理;当target对象是响应式数据时,我们将target的属性key也设置为响应式并手动触发通知其属性值的更新;
上面代码中最重要的就是如下代码:
defineReactive(ob.value, key, val)
ob.dep.notify()
这两行是将新增属性设置为响应式,并手动触发通知该属性值的更新,这就是通过this.$set()
设置之后新增的属性会变成响应式并及时体现在页面中的原因。
[1] 从vue源码看Vue.set()和this.$set()