【Vue】vuejs中怎么给vm实例动态添加响应式属性?
vue.js官网中相关章节是这么解释的:
链接为:https://cn.vuejs.org/v2/guide...
有时你可能需要为已有对象赋予多个新属性,比如使用 Object.assign() 或
_.extend()。在这种情况下,你应该用两个对象的属性创建一个新的对象。所以,如果你想添加新的响应式属性,不要像这样:Object.assign(vm.userProfile, {
age: 27,
favoriteColor: 'Vue Green'
})
你应该这样做:
vm.userProfile = Object.assign({}, vm.userProfile, {
age: 27,
favoriteColor: 'Vue Green'
})
我用代码测试了一下,确实如此,但不明白为什么会这样,很奇怪,来个大神给解释下吧。
回答
两种写法的userProfile
得到的结果虽然是一样的。
还是官网那个页面,前面有提到--- 对于已经创建的实例,Vue 不能动态添加根级别的响应式属性
var vm = new Vue({ data: {
a: 1
}
})
// `vm.a` 现在是响应式的
vm.b = 2
// `vm.b` 不是响应式的
问题中的第一种写法相当于vm.b = 2
,对于已经创建的实例userProfile
,对于在userProfile
上添加属性,Vue
不能动态的检测到。
第二种写法相当于vm.a = XX
,先把两个对象的属性赋给一个空的对象,然后再把这个对象赋给userprofile
, 这是直接为根级别的对象重新赋值,这与对对象属性的添加与删除本质是不一样的。这是我的想法。
赞同上一楼,其实已经解释得差不多了,我再补充一些Object.assign的相关知识吧
if (desc !== undefined && desc.enumerable) { to[nextKey] = nextSource[nextKey];
}
从Object的polyfill实现可以看出,属性的复制通过赋值实现(这样子做的缺点在于只有根属性是深拷贝,其他都是浅拷贝)
这样子的写法其实和vm.b = 2
没有什么差别,都属于在已经创建的实例上面对其添加属性,而Vue并没有对这些属性执getter/setter 转化过程,所以也无法做到的这些属性的数据双向绑定,而为什么对vm.userProfile赋值可以触发这个机制,没有看过源码暂不得知。
附上官方的说明:
Vue 不允许在已经创建的实例上动态添加新的根级响应式属性 (root-level reactive property)。然而它可以使用 Vue.set(object, key, value) 方法将响应属性添加到嵌套的对象上。
有时你想向已有对象上添加一些属性,例如使用 Object.assign() 或 _.extend() 方法来添加属性。但是,添加到对象上的新属性不会触发更新。在这种情况下可以创建一个新的对象,让它包含原对象的属性和新的属性。
你这个问题问得非常好,我之前都没注意到这个细节。。为了解答你的问题,我从Vue的原理开始解释可能会更好地发现这个原因:
PS:可以关注我的文章Vue原理剖析,自己写个vue更加容易理解。
我们都知道Vue的响应式原理用到了Object.defineProperty
这个属性,我们可以模仿Vue写一个定义响应式的方法:
function defineReactive (obj, key, value){ Object.defineProperty(obj,key,{
get:function(){
console.log("get了值"+JSON.stringify(value));
return value;//获取到了值
},
set:function(newValue){
if(newValue === value){
return;//如果值没变化,不用触发新值改变
}
value = newValue;//改变了值
console.log("set了最新值"+JSON.stringify(value));
}
})
}
上面的方法,我们可以看到它的原理是通过Object.defineProperty
声明Vue的data
实例来声明响应式,我们可以在其中拦截到这个get
和set
的方法。
那么,这个时候我们声明一个对象obj
:
let obj = { userProfile: {
name: '我是姓名'
}
}
你可以把这个obj
理解成vue
里面的data
属性。
然后,我们声明响应式:
Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]);
})
如果采用第一个方法:
Object.assign(obj.userProfile, { age: '新添加了属性age'
})
obj.userProfile.age = '改了年龄这个属性';
我们可以发现输出的结果是这样的:
我们可以发现,只有拦截到它的get
方法,并没有触发set
方法!也就是没有触发响应式。
但是!当我们用第二种方法的时候:
obj.userProfile = Object.assign({}, obj.userProfile, { age: '新添加了属性age'
});
obj.userProfile.age = '改了年龄这个属性';
你会发现输出是这样的:
这时候惊喜的发现,触发了set方法,达到了响应式的目的!
所以根据我的结论:由于Object.defineProperty
这个方法的特殊性,所以导致响应式数据的内部深度属性监听实现有点麻烦,所以用上述其他几位回答的原理,可以达到新对象直接赋值的目的,从而间接实现了响应式
编辑了,原来举的例子是错误的,官方声明如下:
根本原因是vue使用Object.defineProperty()重新s定义vue的实例对象vue源码,它跟普通的对象就有了区别(这里使用了发布订阅者模式,而不同推向没有这个),你像操作普通对象那样操作vue实例对象,肯定不合适了。而vue给了我们api Vue.$set,Vue.$delete
来操作属性,它们继承了Vue.prototype。而你说的 Object.assign({}, vm.userProfile,{...})
就是让一个普通对象拥有了vue实例对象的响应式特点,关键是你没明白 Object.assign()
的特点, MDN.aspx)。
看你还没有采纳上述诸多答案,似乎题主没有看到自己想要的答案,那我也来凑凑数。
从题主提出的问题来看,题主也是个好奇宝宝,而我也觉得有点意思,顺道也看了上述大佬的一些回答,不过我不解释原理,请跟我来。
写在前面
本次作答调试所用的项目fork自 @安静的木马 分享的项目
https://codepen.io/rushui/ful...
https://codepen.io/rushui/ful...
只是做了少许改动,为了调试,加了debugger且修改了引入的vue.js。
开始调试第一个链接
当你打开上述俩个链接的控制台并刷新页面时,你便进入了我设置的断点处。
我们先进行第一个 https://codepen.io/rushui/ful... 的调试
先看这种情况进入断点会被vue怎么处理,
进入vm.userProfile的代理getter
这里,sourceKey是_data, key是userProfile, sharedPropertyDefinition可以理解为vue处理数据的一个代理对象。
最终代理getter取了vm["_data"]["userProfile"]的对象值,值得注意的是,这个对象值里面是只有在初始化实例的时候定义的name,以及vue赋予的可响应式getter和setter。
进入vm.userProfile的getter
接下来就会进入userProfile的可响应式getter中,如图
最终返回了这个对象,这是Object.assign中参数vm.userProfile的读取就完成了
进入vm.userProfile的setter
下面该是Object.assign执行之后,进入vm.userProfile的setter了
关键的地方在这里,请看下图
大家看到了吗,新的值对象newVal和旧的值对象val是不一样的,并且vue在这里做了检测,如果这俩个值一样(并处理了NaN这个诡异的东东了,作者还是非常谨慎的,??),那就直接返回,因为你赋值没改啥就不处理这些属性了(一会我们会发现题主说的另一种方式这俩个值是一样的),当前情况现在发现俩值不一样,那么就会进行后续逻辑,我们往下继续。
新的对象没有自定义的setter
,源对象也没有setter描述符定义
,走到1017行
,直接把没有响应式的新对象赋给val了,丢弃了旧的vue处理过响应式的对象。
对新对象进行响应式观察
接下来,1019行
,observe(newVal)
observe方法做了一系列判断,就为了创建一个observer instance.
最终创建了这个新对象的observer实例,
遍历新对象的所有属性并迭代观察
创建完之后,this.walk这个方法又将其属性值深度遍历了一遍进行了响应式观察。
至此,本次调试结束。
调试1结论
从上面看出,如果是一个新对象,那么vue会重新处理这个值对象的属性的响应式。
第二个调试
接下来我们看看第二个与第一个的不同点
https://codepen.io/rushui/ful...
打开调试
离奇的没有进入setter
发现,并没有进入vue在1004行
给userProfile定义的响应式setter中, 如下图(复制的上面调试的图,只想显示一下这个setter, 显示的值不是本次调试的内容,请忽略)
这有点奇怪,而MDN上这样描述Object.assign,按描述应该调用才对,一脸懵逼?,求大神解答。Object.assign(target, ...sources)
Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。该方法使用源对象的[[Get]]和目标对象的[[Set]],所以它会调用相关 getter 和 setter。
如果没有调用的话,那么我们看下图,
到28行的时候,userProfile对象只有name属性有响应式getter和setter而age是没有的,因为vue没有机会给age添加响应式getter和setter, 因为age属性在加到userProfile对象上的时候没有调用userProfile的setter。。。
强制进入setter
于是我增加了28行下面的代码,强制让他走一下userProfile的setter,可不可以呢。
为了让前面的属性定义不影响后面的调试,我们再来一个项目(有点多哦),
https://codepen.io/rushui/ful...
我们继续这个项目的调试,这次走到setter了,如下图
但是没逃过作者定义的判断,那就是这俩个值如果相等就直接返回。 可是这俩个值为什么相等呢?
为什么这次调试 val和newVal是相等的
val和newVal为啥是一模一样的,原来Object.assign的方式吧属性都合并到userProfile这个对象上了,而userProfile实际上是一个指针,指向了内存中的一个对象,而这个对象只有一个,所以当你合并完成,这个对象的引用并没有发生变化,内存中的对象增加了属性age,而userProfile存储的值本身就是地址,这个地址没有变化,所以前后取得都是同一个对象。而这个对象在当前情况下已经被Object.assign给更新为最新的了。
引申出了一个问题,vue作者为啥不拿val的快照做比较,而是拿引用做比较?一脸懵逼?。
总结一下
所以至此我的调试完毕,后面俩中方式都不可行,都会被终止,而没有进入到vue处理属性的setter和getter中。
以上是 【Vue】vuejs中怎么给vm实例动态添加响应式属性? 的全部内容, 来源链接: utcz.com/a/73377.html