【源码解析】一起了解下$set添加响应式属性的原理

503 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

前言

大家好,上一篇文章MVVM中我们共同学习了Vue2中MVVM的原理,并用一个极其简单的案例模拟实现了一下mvvm的基本流程。今天我们将继续学习一下vue2中set方法的源码及原理。在分析源码之前我们先来看下这个set方法的源码及原理。在分析源码之前我们先来看下这个set方法是干嘛用的,为什么要有这么个方法存在。

案例

大家都知道:响应式是vue中最大的特点之一,就比如说当我们把data中定义的某个属性(name)值修改后,对应的用到该属性(name)的页面上也会同步更新,同样也可以通过页面上的表单元素进行反向修改(即修改了表单元素的值,对应的绑定的属性name也会同步更新)。

但是这种情况也是有条件的,前提就是该属性必须已经提前定义在data中了。

还有另外一种情况,我们用一个案例来模拟一下:比如在data中定义一个obj对象,该对象包含一个name属性,然后再将该对象通过小胡子语法显示在页面上,那么毫无疑问这个name属性肯定是响应式的双向都可以修改,如下代码和效果图:

<div id="app">
    obj: {{obj}}
</div>
const vm = new Vue({
    el:"#app",
    data:{
        obj:{
            name:"Alvin"
        }
    }
});

image.png 现在又来了个新需求:要求我们通过点击一个按钮动态为obj对象再添加一个age属性,看上去好像也挺简单,就是在页面上添加一个按钮并绑定一个click事件,然后再在绑定的事件中给obj添加一个age属性即可,如下:

<button @click="addAge">add age</button>
const vm = new Vue({
    methods:{
        addAge(){
            this.obj.age = 18;
        }
    }
});

然而,当我们把代码运行起来发现点击按钮却没有任何反应,貌似age属性没有添加成功,然后打开控制台发现实际上age已经添加进去了,但却不是响应式的(没有三个点)也就是说虽然age添加成功了但却没有被劫持因此也就不能双向绑定了。如下图: image.png

这是因为vue仅在初始化的时候对已经定义了的属性只进行一次数据劫持,后面再添加的属性就不再进行劫持了,这也正是为什么age属性添加成功了但却不是响应式的原因。

那么如果我们就是想要动态添加一个响应式的属性就没办法了吗,当然不是的。为了解决这个问题vue为我们提供了一个额外的专门用来添加响应式属性的方法set,也就是本次我们要分享的核心(铺垫了这么多主人公终于出场了)。下面我们把this.obj.age=18改成用set,也就是本次我们要分享的核心(铺垫了这么多主人公终于出场了)。 下面我们把 this.obj.age = 18改成用set的形式添加再来看下效果:

addAge(){
    //该方法接收3个参数:第一个是要添加属性的对象,第二个是要添加的属性名,第三个参数属性值
    this.$set(this.obj,'age',18);
}

这样修改后我们再来看新添加的age属性就已经显示在页面上了。 image.png

$set源码

通过上面的案例我们知道通过set我们可以添加一个响应是的属性,那么它的实现原理是什么呢,下面我们通过set我们可以添加一个响应是的属性,那么它的实现原理是什么呢,下面我们通过set的源码来解读一下:下载到vue2的源码后找到src/core/instance/observer/index.js文件,在该文件中有如下代码片段就是$set的源码,一共30来行看上去还是比较简单的。

//该方法接收三个参数,target:要添加属性的对象,key:属性名,val:属性值
function set (target, key, val) {
    //首先判断target不能是undefine,null或原始值,如果是这三者之一则抛出错误
    if (isUndef(target) || isPrimitive(target)
    ) {
      warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
    }
    //判读target是否是一个数组,如果是数组并且key是一个一个有效索引则进行单独处理,
    //vue2中数组和对象的数据劫持的实现方式不同
    if (Array.isArray(target) && isValidArrayIndex(key)) {
      target.length = Math.max(target.length, key);
      target.splice(key, 1, val);//通过数组的splice方法添加一个新值
      return val
    }
    //如果要添加的属性已经在对象中存在并且不是对象的原生属性,则直接进行赋值操作
    if (key in target && !(key in Object.prototype)) {
      target[key] = val;
      return val
    }
    // __ob__是vue中的内置属性,包括dep、value和vmCount三个值,
    // 其中dep就是我们前面分享mvvm时的Dep的实例用来做依赖收集和通知模板更新的,
    // 而value的值其实就是传进来的target
    var ob = (target).__ob__;
    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
    }
    //如果ob不存在,则直接做赋值操作
    if (!ob) {
      target[key] = val;
      return val
    }
    //接下来则是$set函数实现添加响应式属性的核心代码,同样这个方法在我们分享mvvm时也已经分析过了,
    //该方法接收三个参数,ob.value上面我们说了其实就是target用于添加响应式属性的对象,
    // key是要添加的响应式属性的名称,val则是对应属性的值。在该方法内容通过原生的Object.property方法
    // 对属性进行数据劫持,并通过dep.depend方法进行依赖收集
    defineReactive$$1(ob.value, key, val);
    //然后通过dep实例中的notify方法通知视图更新
    ob.dep.notify();
    return val
  }

总结

今天的分享我们通过一个简单案例分析学习了如果给对象添加一个响应式属性,从而引出了vue中的set方法,并通过对set方法,并通过对set源码的分析我们知道了set的实现原理,知道了为什么set的实现原理,知道了为什么set就可以添加响应式属性。好了,本次分享就到这里了。