vue源码分析(三)——数据劫持 与 数据绑定

Object

1、数据绑定(model==>View):
一旦更新了data中的某个属性数据, 所有界面上直接使用或间接使用了此属性的节点都会更新(更新)

2、数据劫持

①. 数据劫持是vue中用来实现数据绑定的一种技术
②. 基本思想: 通过defineProperty()来监视data中所有属性(任意层次)数据的变化, 一旦变化就去更新界面

3、四个重要对象

①. Observer

  • 通过过隐式递归调用实现所有层次属性的监视/劫持
  • 给data重新定义属性, 添加 setter / getter
  • 为data中的每个属性创建对应的dep对象

②. Dep(Depend)

  • data中的每个属性(所有层次)都对应一个dep对象
  • 创建的时机:
    • 在初始化define data中各个属性时创建对应的dep对象
    • 在data中的某个属性值被设置为新的对象时
  • 对象的结构
	Dep{
		id, // 每个dep都有一个唯一的id
		subs //包含n个对应watcher的数组(subscribes的简写)
	}
  • subs属性说明
    • 当一个watcher被创建时, 内部会将当前watcher对象添加到对应的dep对象的subs中
    • 当此data属性的值发生改变时, 所有subs中的watcher都会收到更新的通知, 调用update 从而最终更新对应的界面

③. Compile

  • 用来解析模板页面的对象的构造函数(一个实例)
  • 利用compile对象解析模板页面
  • 插值语法和一般指令语法都会调用bind ,bind 中会创建 watcher
  • 每解析一个表达式(非事件指令)都会创建一个对应的watcher对象, 并建立watcher与dep的关系
  • complie与watcher关系: 一对多的关系

④. Watcher

  • 模板中每个非事件指令或表达式都对应一个watcher对象
  • 每个watcher 都包含一个用于更新对应节点的回调函数
  • 创建的时机 : 在初始化编译模板时
  • 对象的组成
	Watcher {
		vm,  //vm对象
		exp, //对应指令的表达式
		cb, //当表达式所对应的数据发生改变的回调函数
		value, //表达式当前的值
		depIds //表达式中各级属性所对应的dep对象的集合对象
				//属性名为dep的id, 属性值为dep
	}

⑤. 总结: dep与watcher的关系: 多对多

  • 一个data中的属性对应对应一个dep, 一个dep中可能包含多个watcher(模板中有几个表达式使用到了属性)
  • 模板中一个非事件表达式对应一个watcher, 一个watcher中可能包含多个dep
  • 数据绑定使用到2个核心技术
    • defineProperty()
    • 订阅者-发布者

4、双向数据绑定

①. 双向数据绑定是建立在单向数据绑定(model==>View)的基础之上的

②. 双向数据绑定的实现流程:

  • 在解析v-model指令时, 给当前元素添加input监听
  • 当input的value发生改变时, 将最新的值赋值给当前表达式所对应的data属性

源码

observer.js:

function Observer(data) {
    // 保存data
    this.data = data;
    // 启动对data对象中数据的劫持
    this.walk(data);
}

Observer.prototype = {
    walk: function(data) {
        var me = this;
        // 遍历data的所有属性
        Object.keys(data).forEach(function(key) {
            // 将data中属性重新定义的响应式
            me.defineReactive(data, key, data[key])
        });
    },

    defineReactive: function(data, key, val) {
        // 创建一个对应的dep对象(订阅器/中间人)
        var dep = new Dep();
        // 通过隐式递归调用实现所有层次属性的监视/劫持
        var childObj = observe(val);

        // 给data重新定义属性, 添加setter/getter
        Object.defineProperty(data, key, {
            enumerable: true, // 可枚举
            configurable: false, // 不能再define
            get: function() {

                // 用于建立dep与watcher的关系
                if (Dep.target) {
                    dep.depend();
                }
                return val;
            },
            set: function(newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                // 偿试监视新的值的内部数据
                childObj = observe(newVal);
                // 通知订阅者
                dep.notify();
            }
        });
    }
};

function observe(value, vm) {

    if (!value || typeof value !== 'object') {
        return;
    }
    // 创建一个对应的observer对象
    return new Observer(value);
};


var uid = 0;

function Dep() {
    this.id = uid++;
    this.subs = [];
}

Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },

    depend: function() {
        Dep.target.addDep(this);
    },

    removeSub: function(sub) {
        var index = this.subs.indexOf(sub);
        if (index != -1) {
            this.subs.splice(index, 1);
        }
    },

    notify: function() {
        // 遍历每个订阅者watcher
        this.subs.forEach(function(sub) {
            // 去更新对应的节点
            sub.update();
        });
    }
};

Dep.target = null;
watch.js:

function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    this.depIds = {};
    // 读取当前表达式对应的属性值
    this.value = this.get();
}

Watcher.prototype = {
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.get();
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            // 调用绑定的更新节点的回调函数
            this.cb.call(this.vm, value, oldVal);
        }
    },
    addDep: function(dep) {
        // 判断watcher与dep的关系是否已经建立过
        if (!this.depIds.hasOwnProperty(dep.id)) {
            // 将watcher添加dep中, 建立dep到watcher的关系
            dep.addSub(this);
            // 将dep添加到watcher中, 建立watcher到dep的关系
            this.depIds[dep.id] = dep;
        }
    },
    get: function() {
        // 将当前watcher对象挂到Dep上
        Dep.target = this;
        // 读取表达式对应的属性值 ==> 调用对应的getter
        var value = this.getVMVal();
        Dep.target = null;
        return value;
    },

    getVMVal: function() {
        var exp = this.exp.split('.');
        var val = this.vm._data;
        exp.forEach(function(k) {
            val = val[k];
        });
        return val;
    }
};

总结

首先,我们通过Object.defineProperty方法实现了对object数据的可观测,并且封装了Observer类,让我们能够方便的把object数据中的所有属性(包括子属性)都转换成getter/seter的形式来侦测变化。

接着,我们学习了什么是依赖收集?并且知道了在getter中收集依赖,在setter中通知依赖更新,以及封装了依赖管理器Dep,用于存储收集到的依赖。

最后,我们为每一个依赖都创建了一个Watcher实例,当数据发生变化时,通知Watcher实例,由Watcher实例去做真实的更新操作。

其整个流程大致如下:

Vue 的响应式是通过 Object.defineProperty 对数据进行劫持,并结合观察者模式实现。
Vue 创建了一个 observe 来劫持监听所有的属性,通过遍历,把这些属性包括子属性全部添加了 getter 和 setter。
Vue 它会在组件渲染的过程中(模板解析)把使用过的data属性所对用的 watcher 通过 getter 收集到 Dep 中,作为依赖。之后当数据发生了变化时,触发setter,会调用 dep 通知 所对应的 watcher,然后 watcher 调用更新的回调函数来更新节点。

触发Watch对象的update实现
当异步执行update的时候,会调用queueWatcher函数。异步推送到观察者队列中,下一个tick时调用
Watch对象并不是立即更新视图,而是被push进了一个队列queue,此时状态处于waiting的状态,这时候会继续会有Watch对象被push进这个队列queue,等待下一个tick时,这些Watch对象才会被遍历取出,更新视图。同时,id重复的Watcher不会被多次加入到queue中去,因为在最终渲染时,我们只需要关心数据的最终结果。
vue源码分析(三)——数据劫持 与 数据绑定_第1张图片

Object.defineProperty的不足

虽然我们通过Object.defineProperty方法实现了对object数据的可观测,但是这个方法仅仅只能观测到object数据的取值及设置值,当我们向object数据里添加一对新的key/value或删除一对已有的key/value时,它是无法观测到的,导致当我们对object数据添加或删除值时,无法通知依赖,无法驱动视图进行响应式更新,需要手动进行 Observe,需要重新遍历对象,对其新增属性再使用 Object.defineProperty 进行劫持。

所以,在使用 Vue 给 data 中的数组或对象新增 / 删除属性时,需要使用 vm.$ set / vm.$ delete才能保证新增 / 删除的属性也是响应式的。


Array

Array型数据设计一套另外的变化侦测机制。

由于数组arr的索引值恰好就是arrObj的key值,所以我们通过数组的索引值来操作数组时是可以用Object.defineProperty监测到的。但是,数组并不是只能由索引值来操作数组,更常用的操作数组的方法是使用数组原型上的一些方法如(push,pop,shift,unshift,splice,sort,reverse)等来操作数组,当使用这些数组原型方法来操作数组时,Object.defineProperty就监测不到了,所以Vue对Array型数据单独设计了数据监测方式。

Array型数据还是在getter中收集依赖。

在Vue中创建了一个数组方法拦截器,它拦截在数组实例与Array.prototype之间,在拦截器内重写了操作数组的一些方法,并将这些方法赋值给了数据的 __ proto __ 上,因为原型链的机制,找到对应的方法就不会继续往上找原生的方法了,当数组实例使用操作数组方法时,其实使用的是拦截器中重写的方法,而不再使用Array.prototype上的原生方法。
拦截器生效以后,当数组数据再发生变化时,我们就可以在拦截器中调用 dep 通知 所对应的 watcher。

编译方法中会对一些会增加索引的方法(push,unshift,splice)进行手动 observe。

在 Array 中,用拦截器代替了setter。

你可能感兴趣的:(vue)