Vue2中响应式中的Vue.$set
前言
在最近的项目中发现表格没有渲染出数据,最后发现是因为数据是非响应式的,由此引发的本文的编写。
Vue中的响应式
先看文档中的三段话:
1、当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。
2、受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
3、Vue 实例的数据对象。Vue 将会递归将 data 的属性转换为 getter/setter,从而让 data 的属性能够响应数据变化。
对象必须是纯粹的对象 (含有零个或多个的 key/value 对):浏览器 API 创建的原生对象,原型上的属性会被忽略。大概来说,data 应该只能是数据 - 不推荐观察拥有状态行为的对象。
一旦观察过,不需要再次在数据对象上添加响应式属性。因此推荐在创建实例之前,就声明所有的根级响应式属性。
一般情况下,在Vue实例data选项中声明的对象,Vue在初始化时遍历data选项的属性对象,添加getter/setter和监听器,用于在某个属性值变化时触发监听更新界面。
所以在初始化完成之后给实例添加的属性如:
1、添加不存在的属性.name, ['name']
2、数组根据索引修改数据 [index]
此类方式没有触发添加目标对象的getter/setter操作,所以他们不是响应式的。
类似官网上的一段代码
var vm = new Vue({
data:{
a:1
}
})
// `vm.a` 是响应式的
vm.b = 2
// `vm.b` 是非响应式的
那如何动态添加响应式属性呢?好在Vue给我们提供了方法。
Vue.set(this.someObject, newProperty, value)
vm.$set(this.someObject, newProperty, value)
this.someObject = Object.assign({}, this.someObject, sourceObject)
set
Vue提供了set
和$set
让我们方便解决动态更改属性的问题,其中$set
是set
的别名,让我们看看这个方法的源码,我加上了自己的注释:
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
* 设置对象的属性。添加新属性并在属性不存在时触发更改通知
* 返回成功添加的值
*/
function set (target, key, val) {
//判断对象是不是为空 是不是原始类型的值
if (isUndef(target) || isPrimitive(target)
) {
warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
}
//判断对象是不是数组 key是不是有效索引 即能不能转成整数
if (Array.isArray(target) && isValidArrayIndex(key)) {
//设置数组的长度 根据数组当前长度和设置的索引取两者中大的
target.length = Math.max(target.length, key);
//替换当前索引值
target.splice(key, 1, val);
return val
}
//判断对象自身和原型中有没有该属性 如果有直接赋值并返回
//防止了重复设置getter/setter
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val
}
//获取对象的观察者
var ob = (target).__ob__;
//判断对象是不是Vue实例 有观察者并且该实例使用次数大于0
//对象是Vue实例 或者 已经在使用 发出错误信息
//不能再运行时给Vue实例或其根$data添加响应式属性
if (target._isVue || (ob && ob.vmCount)) {
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
}
//添加getter/setter
defineReactive$$1(ob.value, key, val);
//通知更新
ob.dep.notify();
//返回设置的值
return val
}
以上注释全是个人理解,其实刚开始在使用时疑问会不会添加多次getter/setter方法,然后就去看了源码,发现并不会重复添加,放心的同时,强行的把整个方法分析了一遍。
defineReactive$$1
这个方法有兴趣可以看一下。
Object.assign()
set可以在添加一个属性的时候使用,此时并不会改变对象的引用。
如果要添加多个属性就要用Object.assign()
如文档中说的:
有时你可能需要为已有对象赋值多个新属性,比如使用 Object.assign() 或 _.extend()。但是,这样添加到对象上的新属性不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的属性一起创建一个新的对象。
// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
不知道大家有没有疑问为什么
Object.assign(this.someObject, { a: 1, b: 2 })
不是响应式,
Object.assign({}, this.someObject, { a: 1, b: 2 })
是响应式。
其实他们的区别是目标对象的不同。
Object.assign(this.someObject, { a: 1, b: 2 })
的目标对象是已经是响应式的this.somObject
,在上边的set
中我们知道通过vm.$set
或Vue.set
可以给响应式对象添加新的响应式属性,但是Object.assign并没有这些操作,所以添加的属性不是响应式的。
Object.assign({}, this.someObject, { a: 1, b: 2 })
的目标对象是一个新对象,对象内存地址都是新的,所以下面代码
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
就相当于更改响应式对象this.someObject的值,调用它的setter方法(开头三段话),此时Vue会遍历新对象的属性,添加getter/setter,从而实现添加多个属性并且是响应式的。
总结
- 根级响应式属性一定要在data中设置,才能响应式的添加属性或修改对象引用。
- Vue.set()/vm.set()可以方便的给响应式对象添加响应式属性。
- Object.assign()目标对象应设置为空对象,赋值的时候强行触发setter方法,达到添加多个响应式属性的目的。
参考文档:
深入响应式原理
Vue api 选项和数据 data