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'
})

我用代码测试了一下,确实如此,但不明白为什么会这样,很奇怪,来个大神给解释下吧。

阅读 935
评论 2018-10-02 提问
    5 个回答

    两种写法的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, 这是直接为根级别的对象重新赋值,这与对对象属性的添加与删除本质是不一样的。这是我的想法。

    评论 赞赏 2018-10-02

      赞同上一楼,其实已经解释得差不多了,我再补充一些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() 方法来添加属性。但是,添加到对象上的新属性不会触发更新。在这种情况下可以创建一个新的对象,让它包含原对象的属性和新的属性。
      评论 赞赏 2018-10-02
        Leon
        • 1.5k

        你这个问题问得非常好,我之前都没注意到这个细节。。为了解答你的问题,我从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实例来声明响应式,我们可以在其中拦截到这个getset的方法。
        那么,这个时候我们声明一个对象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这个方法的特殊性,所以导致响应式数据的内部深度属性监听实现有点麻烦,所以用上述其他几位回答的原理,可以达到新对象直接赋值的目的,从而间接实现了响应式

        测试响应式,F12控制台输出查看


        编辑了,原来举的例子是错误的,官方声明如下:
        图片描述

        评论 赞赏 2018-10-03

          根本原因是vue使用Object.defineProperty()重新s定义vue的实例对象vue源码,它跟普通的对象就有了区别(这里使用了发布订阅者模式,而不同推向没有这个),你像操作普通对象那样操作vue实例对象,肯定不合适了。而vue给了我们api Vue.$set,Vue.$delete来操作属性,它们继承了Vue.prototype。而你说的 Object.assign({}, vm.userProfile,{...}) 就是让一个普通对象拥有了vue实例对象的响应式特点,关键是你没明白 Object.assign() 的特点, MDN.aspx)。

          评论 赞赏 2018-10-03
            言月
            • 1.5k

            看你还没有采纳上述诸多答案,似乎题主没有看到自己想要的答案,那我也来凑凑数。

            从题主提出的问题来看,题主也是个好奇宝宝,而我也觉得有点意思,顺道也看了上述大佬的一些回答,不过我不解释原理,请跟我来。

            写在前面

            本次作答调试所用的项目fork自 @安静的木马 分享的项目
            https://codepen.io/rushui/ful...
            https://codepen.io/rushui/ful...
            只是做了少许改动,为了调试,加了debugger且修改了引入的vue.js。

            开始调试第一个链接

            当你打开上述俩个链接的控制台并刷新页面时,你便进入了我设置的断点处。
            我们先进行第一个 https://codepen.io/rushui/ful... 的调试

            clipboard.png

            先看这种情况进入断点会被vue怎么处理,

            进入vm.userProfile的代理getter

            clipboard.png

            这里,sourceKey是_data, key是userProfile, sharedPropertyDefinition可以理解为vue处理数据的一个代理对象。
            最终代理getter取了vm["_data"]["userProfile"]的对象值,值得注意的是,这个对象值里面是只有在初始化实例的时候定义的name,以及vue赋予的可响应式getter和setter。

            进入vm.userProfile的getter

            接下来就会进入userProfile的可响应式getter中,如图

            clipboard.png

            最终返回了这个对象,这是Object.assign中参数vm.userProfile的读取就完成了

            进入vm.userProfile的setter

            下面该是Object.assign执行之后,进入vm.userProfile的setter了
            关键的地方在这里,请看下图

            clipboard.png

            大家看到了吗,新的值对象newVal和旧的值对象val是不一样的,并且vue在这里做了检测,如果这俩个值一样(并处理了NaN这个诡异的东东了,作者还是非常谨慎的,👍👍),那就直接返回,因为你赋值没改啥就不处理这些属性了(一会我们会发现题主说的另一种方式这俩个值是一样的),当前情况现在发现俩值不一样,那么就会进行后续逻辑,我们往下继续。
            新的对象没有自定义的setter,源对象也没有setter描述符定义,走到1017行,直接把没有响应式的新对象赋给val了,丢弃了旧的vue处理过响应式的对象。

            对新对象进行响应式观察

            接下来,1019行,observe(newVal)

            observe方法做了一系列判断,就为了创建一个observer instance.

            clipboard.png

            最终创建了这个新对象的observer实例,

            clipboard.png

            遍历新对象的所有属性并迭代观察

            创建完之后,this.walk这个方法又将其属性值深度遍历了一遍进行了响应式观察。

            clipboard.png

            至此,本次调试结束。

            调试1结论

            从上面看出,如果是一个新对象,那么vue会重新处理这个值对象的属性的响应式。

            第二个调试

            接下来我们看看第二个与第一个的不同点

            https://codepen.io/rushui/ful...

            打开调试

            离奇的没有进入setter

            发现,并没有进入vue在1004行给userProfile定义的响应式setter中, 如下图(复制的上面调试的图,只想显示一下这个setter, 显示的值不是本次调试的内容,请忽略)

            clipboard.png

            这有点奇怪,而MDN上这样描述Object.assign,按描述应该调用才对,一脸懵逼😳,求大神解答。
            Object.assign(target, ...sources)

            Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。该方法使用源对象的[[Get]]和目标对象的[[Set]],所以它会调用相关 getter 和 setter。

            如果没有调用的话,那么我们看下图,
            clipboard.png

            到28行的时候,userProfile对象只有name属性有响应式getter和setter而age是没有的,因为vue没有机会给age添加响应式getter和setter, 因为age属性在加到userProfile对象上的时候没有调用userProfile的setter。。。

            强制进入setter

            于是我增加了28行下面的代码,强制让他走一下userProfile的setter,可不可以呢。
            为了让前面的属性定义不影响后面的调试,我们再来一个项目(有点多哦),

            https://codepen.io/rushui/ful...

            我们继续这个项目的调试,这次走到setter了,如下图

            clipboard.png

            但是没逃过作者定义的判断,那就是这俩个值如果相等就直接返回。 可是这俩个值为什么相等呢?

            为什么这次调试 val和newVal是相等的

            val和newVal为啥是一模一样的,原来Object.assign的方式吧属性都合并到userProfile这个对象上了,而userProfile实际上是一个指针,指向了内存中的一个对象,而这个对象只有一个,所以当你合并完成,这个对象的引用并没有发生变化,内存中的对象增加了属性age,而userProfile存储的值本身就是地址,这个地址没有变化,所以前后取得都是同一个对象。而这个对象在当前情况下已经被Object.assign给更新为最新的了。
            引申出了一个问题,vue作者为啥不拿val的快照做比较,而是拿引用做比较?一脸懵逼😳。

            总结一下

            所以至此我的调试完毕,后面俩中方式都不可行,都会被终止,而没有进入到vue处理属性的setter和getter中。

            评论 赞赏 2018-10-03
              撰写回答

              登录后参与交流、获取后续更新提醒