【Vue原理】依赖收集 - 源码版之引用数据类型

684 阅读8分钟

写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版本 【2.5.17】

如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧

【Vue原理】依赖收集 - 源码版之引用数据类型

上一篇,我们已经分析过了 基础数据类型的 依赖收集

【Vue原理】依赖收集 - 源码版之基本数据类型

这一篇内容是针对 引用数据类型的数据的 依赖收集分析,因为引用类型数据要复杂些,必须分开写

文章很长,高能预警,做好准备耐下心好,肯定还是有点收获的

但是两个类型的数据的处理,又有很多重复的地方,所以打算只写一些差异性的地方就好了,否则显得废话很多

两个步骤,都有不同的地方

1、数据初始化

2、依赖收集


数据初始化流程

如果数据类型是引用类型,需要对数据进行额外的处理。

处理又分了 对象 和 数组 两种,会分开来讲

1对象

1、遍历对象的每个属性,同样设置响应式,假设属性都是基本类型,处理流程跟上一篇一样

2、每个数据对象会增加一个 ob 属性

比如设置一个 child 的数据对象

image

下图,你可以看到 child 对象处理之后添加了一个 ob 属性

image

ob_ 属性有什么用啊?

你可以观察到,ob 有一个 dep 属性,这个 dep 是不是有点属性,是的,在上一篇基础数据类型中讲过

那么这个 ob 属性有什么用啊?

你可以观察到,ob 有一个 dep 属性,这个 dep 是不是有点属性,是的,在上一篇基础数据类型中讲过

dep 正是存储依赖的地方

比如 页面引用了 数据child,watch 引用了数据child,那么child 就会把这个两个保存在 dep.subs 中

dep.subs = [ 页面-watcher,watch-watcher ]

但是,在上一篇基础类型种, dep 是作为闭包存在的啊,并不是保存在什么【ob.dep】 中啊

没错,这就是 引用类型 和 基础类型的区别了

基础数据类型,只使用 【闭包dep】 来存储依赖

引用数据类型,使用 【闭包dep】 和 【 ob.dep】 两种来存储依赖

什么?你说闭包dep 在哪里?好吧,在 defineReactive 的源码中,你去看看这个方法的源码,下面有

那么,为什么,引用类型需要 使用__ob__.dep 存储依赖呢?

首先,明确一点,存储依赖,是为了数据变化时通知依赖,所以 ob.dep 也是为了变化后的通知

闭包 dep 只存在 defineReactive 中,其他地方无法使用到,所以需要保存另外一个在其他地方使用

在其他什么地方会使用呢?

在Vue挂载原型上的方法 set 和 del 中,源码如下

function set(target, key, val) {    

    var ob = (target).__ob__;    

    // 通知依赖更新
    ob.dep.notify();
}
Vue.prototype.$set = set;
function del(target, key) {    

    var ob = (target).__ob__;    

    delete target[key];    

    if (!ob)  return

    // 通知依赖更新
    ob.dep.notify();

}
Vue.prototype.$delete = del;

这两个方法,大家应该都用过,为了给对象动态 添加属性和 删除属性

但是如果直接添加属性或者删除属性,Vue 是监听不到的,比如下面这样

child.xxxx=1

delete child.xxxx

所以必须要通过 Vue 包装过的方法 set 和 del 来操作

在 set 和 del 执行完,是需要通知依赖更新的,但是我怎么通知?

此时,【ob.dep】 就发挥作用了!就因为依赖多收集了一份在 ob.dep 中

使用就是上面一句话,通知更新

ob.dep.notify();

2、数组

1、需要遍历数组,可能数组是对象数组,如下面

[{name:1},{name:888}]

遍历时,如果遇到子项是对象的,会跟上面解析对象一样操作

2、给数组保存一个 ob 属性

比如设置一个 arr 数组

公众号

看到 arr数组 加多了一个 ob 属性

公众号

其实这个 ob 属性 和 上一段讲对象 的作用是差不多的,这里也只是说 ob.dep

数组中的 ob.dep 存储的也是依赖,给谁用呢?

给 Vue 封装的数组方法使用,要知道要想数组变化也被监听到,是必须使用Vue封装的数组方法的,否则无法实时更新

这里举重写方法之一 push,其他的还有 splice 等,Vue 官方文档已经有过说明

var original = Array.prototype.push;

Array.prototype.push = function() {    

    var args = [],

    len = arguments.length;    

    // 复制 传给 push 等方法的参数
    while (len--) args[len] = arguments[len];

    // 执行 原方法
    var result = original.apply(this, args);    

    var ob = this.__ob__;    

    // notify change
    ob.dep.notify();    

    return resul
}

看到在执行完 数组方法之后,同样需要通知依赖更新,也就是通知 ob.dep 中收集的依赖去更新

现在,我们知道了,响应式数据对 引用类型做了什么额外的处理,主要是加了一个 ob 属性

我们已经知道了 ob 有什么用,现在看看源码是怎么添加 ob

// 初始化Vue组件的数据

function initData(vm) {    

    var data = vm.$options.data;

    data = vm._data = 

        typeof data === 'function' ? 

        data.call(vm, vm) : data || {};

    ....遍历 data 数据对象的key ,重名检测,合规检测
    observe(data, true);

}

function observe(value) {    

    if (Array.isArray(value) || typeof value == "object") {
        ob = new Observer(value);
    }    
    return ob
}
function Observer(value) {   

    // 给对象生成依赖保存器
    this.dep = new Dep();   

    // 给 每一个对象 添加一个  __ob__ 属性,值为 Observer 实例
    value.__ob__ = this

    if (Array.isArray(value)) { 

        // 遍历数组,每一项都需要通过 observe 处理,如果是对象就添加 __ob__
        for (var i = 0, l =value.length; i < l; i++) {
            observe(value[i]);
        }

    } else {        

        var keys = Object.keys(value);     

        // 给对象的每一个属性设置响应式
        for (var i = 0; i < keys.length; i++) {
            defineReactive(value, keys[i]);
        }
    }
};

源码的流程跟上一篇差不多,只是处理引用数据类型会增加多几行源码的额外处理

我们之前只说了一种对象数据类型,比如下面这样

公众号

如果会嵌套多层对象呢?比如这样,会怎么处理

公众号

没错,Vue 会递归处理,当遍历属性,使用 defineReactive 处理时,递归调用 observe 处理(源码标红加粗)

如果值是对象,那么同样给 值加多一个 ob

如果不是,那么正常往下走,设置响应式

源码如下

function defineReactive(obj, key, value) {  

    // dep 用于中收集所有 依赖我的 东西
    var dep = new Dep();    
    var val  = obj[key] 

    // 返回的 childOb 是一个 Observer 实例
    // 如果值是一个对象,需要递归遍历对象
    var childOb = observe(val);    

    Object.defineProperty(obj, key, {
        get() {...依赖收集跟初始化无关,下面会讲},
        set() { .... }
    });
}

画一个流程图,仅供参考

公众号

哈哈哈,上面写得好长啊,是有点,但是没办法,想说详细点啊,好吧,还有一段,但是比较短一些哈哈哈,反正看完的人,我jio 得很厉害了,答应我,如果你仔细看完了,评论一下好吗,让我知道有人仔细看了


依赖收集流程

收集流程,就是重点关注 Object.defineProperty 设置的 get 方法了

跟 基础类型数据 对比,引用类型的 收集方法也只是多了几行处理,差异在两行代码

childOb.dep.depend,被我 简单化为 childOb.dep.addSub(Dep.target) dependArray(value) 可以先看下源码,如下

function defineReactive(obj, key, value) {    

    var dep = new Dep();    
    var val  = obj[key]    
    var childOb = observe(val);    

    Object.defineProperty(obj, key, {
        get() {            
            var value = val            
            if (Dep.target) {

                // 收集依赖进 dep.subs
                dep.addSub(Dep.target);

                // 如果值是一个对象,Observer 实例的 dep 也收集一遍依赖
                if (childOb) {
                    childOb.dep.addSub(Dep.target)          
                    if (Array.isArray(value)) {
                        dependArray(value);
                    }
                }
            }            
            return value
        }
    });
}

上面的源码,混杂了 对象和 数组的处理,我们分开说

1、对象

在数据初始化的流程中,我们已经知道值是对象的话,会存储多一份依赖在 ob.dep 中

就只有一句话

childOb.dep.depend();

数组还有另外一个处理,就是

dependArray(value);

看下源码,如下

function dependArray(value) {    

    for (var i = 0, l = value.length; i < l; i++) {        

        var e = value[i];        

        // 只有子项是对象的时候,收集依赖进 dep.subs
        e && e.__ob__ && e.__ob__.dep.addSub(Dep.target);   
     

        // 如果子项还是 数组,那就继续递归遍历
        if (Array.isArray(e)) {
            dependArray(e);
        }
    }
}

显然,是为了防止数组里面有对象,从而需要给 数组子项对象也保存一份

你肯定会问,为什么子项对象也要保存一份依赖?

1、页面依赖了数组,数组子项变化了,是不是页面也需要更新?但是子项内部变化怎么通知页面更新?所以需要给子项对象也保存一份依赖?

2、数组子项数组变化,就是对象增删属性,必须用到Vue封装方法 set 和 del,set 和 del 会通知依赖更新,所以子项对象也要保存

看个栗子

公众号

页面模板

公众号

看到数组的数据,就存在两个 ob

公众号


总结

到这里,就可以很清楚,引用类型和 基础类型的处理差异了

1、引用类型会多添加一个 __ob__属性,其中包含 dep,用于存储 收集到的依赖

2、对象使用 ob.dep,作用在 Vue 自定义的方法 set 和 del 中

3、数组使用 ob.dep,作用在 Vue 重写的数组方法 push 等中

终于写完了,真的好长,但是我觉得值得了

公众号

公众号