Vue底层之数据响应式原理(vue2)

329 阅读7分钟

数据响应式原理(vue2)

MVVM

非侵入

Object.defineProperty() 数据劫持/数据代理

let obj ={};
Object.defineProperyty(obj,'property1',{
    value: 1,
    //是否可写  修改
    wirtable: false,
    //是否可以被枚举 for in
    enumerable:false
})

getter/setter

Objecct.defineProperty(obj,'property1',{
    get(){
        console.log('property1被劫持,访问')
    }
    set(){
    	console.log('property1被劫持,改变')
}
})

get和value不能同时设定,因为get的返回值作为属性值

因此setter后修改需要共同的变量,给getter返回

let obj={};
let temp;
Objecct.defineProperty(obj,'property1',{
    get(){
        console.log('property1被劫持,访问');
        return temp;
    }
    set(newValue){
    	console.log('property1被劫持,改变');
    	temp = newValue;  
}
})
 

可以发现上面的temp是外层变量,可以改造成函数来形成闭包 defineReative

function defineReactive(data,key,val){
   Objecct.defineProperty(data,key,{
    enumerable:true,
    //可配置,删除
    configurable:true,
    get(){
        console.log('key访问');
        return val;
    }
    set(newValue){
    	console.log('改变');
    	val = newValue;  
}
}) 
}

问题:对象的深层无法检测,所以需要递归observe

递归侦测对象的全部属性

调用过程 :

外层 index 直接observe(obj)

observe 检测有无Observer类实例 没有则new一个赋给_ ob _

Observer构造函数 调用 walk 静态方法 对obj的内部每个属性进行 defineReactive 调用 (设置getter setter)

在difineReactive每个属性调用observe 这里就实现了递归

注意的是,其中setter的newValue也要observe(设置的值如果是新对象也要进行递归侦测属性)

Observer 类:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object

主要流程: observe(obj) -> obj有无_ ob _ 没有new Observer() 添加到_ ob _ 上 -> 遍历下一层属性,逐个defineReactive

utils类: 工具类, def函数将‘__ ob __’ 设置为无法枚举遍历

//设置enumerable 
export const def = function (obj, key, value, enumerable) {
    Object.defineProperty(obj, key, {
        value,
        enumerable,
        writable: true,
        configurable: true
    });
};

数组的响应式处理

vue中为了处理数组的响应式,改写 七个方法

push pop shift unshift splice sort reverse

静态方法都在Array.prototype 上

我们构造一个对象arrayMethods proto指向这个数组原型

实现: arrayMethods = Object.creat(Array.prototype)

在Observer类内部,如果value是数组,需要将这个数组原型指向这个arrayMethods,

使用Object.setPrototypeOf(value,arrayMethods)

//array.js  处理数组

// 得到Array.prototype
const arrayPrototype = Array.prototype;

// 以Array.prototype为原型创建arrayMethods对象,并暴露
export const arrayMethods = Object.create(arrayPrototype);

// 要被改写的7个数组方法
const methodsNeedChange = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
];

methodsNeedChange.forEach(methodName => {
    // 备份原来的方法,因为push、pop等7个函数的功能不能被剥夺
    const original = arrayPrototype[methodName];
    // 定义新的方法
    def(arrayMethods, methodName, function () {
        // 恢复原来的功能
        const result = original.apply(this, arguments);
        // 把类数组对象变为数组
        const args = [...arguments];
        // 把这个数组身上的__ob__取出来,__ob__已经被添加了,为什么已经被添加了?因为数组肯定不是最高层,比如obj.g属性是数组,obj不能是数组,第一次遍历obj这个对象的第一层的时候,已经给g属性(就是这个数组)添加了__ob__属性。
        const ob = this.__ob__;

        // 有三种方法push\unshift\splice能够插入新项,现在要把插入的新项也要变为observe的
        let inserted = [];

        switch (methodName) {
            case 'push':
            case 'unshift':
                inserted = args;
                break;
            case 'splice':
                // splice格式是splice(下标, 数量, 插入的新项)
                inserted = args.slice(2);
                break;
        }

        // 判断有没有要插入的新项,让新项也变为响应的
        if (inserted) {
            ob.observeArray(inserted);
        }

        console.log('啦啦啦');

        ob.dep.notify();

        return result;
    }, false);
});
export default class Observer {
    constructor(value) {
        // 每一个Observer的实例身上,都有一个dep
        this.dep = new Dep();
        // 给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)添加了__ob__属性,值是这次new的实例
        def(value, '__ob__', this, false);
        // console.log('我是Observer构造器', value);
        // 不要忘记初心,Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
        // 检查它是数组还是对象
        if (Array.isArray(value)) {
            // 如果是数组,要非常强行的蛮干:将这个数组的原型,指向arrayMethods
            Object.setPrototypeOf(value, arrayMethods);
            // 让这个数组变的observe
            this.observeArray(value);
        } else {
            this.walk(value);
        }
    }
    // 遍历
    walk(value) {
        for (let k in value) {
            defineReactive(value, k);
        }
    }
    // 数组的特殊遍历
    observeArray(arr) {
        for (let i = 0, l = arr.length; i < l; i++) {
            // 逐项进行observe
            observe(arr[i]);
        }
    }
};

对象的侦测: observe - observer实例 - walk遍历 - 对每个属性observe并侦听

这个时候能做到 : 对象的所有属性的get和set都能侦听到

数组的侦测: observe - observer实例 - 将数组原型连上 arrayMethods(arrayMethods引入时就开始运行,arrayMethods的原型连上Array.prototype,目的是获取到原型方法并在arrayMethods中defineProperty定义改写的方法,其中push,unshift,splice会增添新元素需要额外进行侦听处理,这个处理即为对增添的元素依次observe ) - 对这个数组的元素进行依次observe

改写方法实现: this为这个数组,通过拿到的原型方法调用得到结果最后返回,中间处理是上文中对增添元素进行依次observe

能做到: 数组调用这些方法能实际生效并对增添的元素(对象或者数组)能够监听,但对于数组内部的项get和set都不能侦听到

tip: 这里是性能权衡后的结果,对于数组的侦测只限于对象和数组,基础数据项都不能侦测

依赖收集

数据需要的地方——依赖

vue1 细粒度 用到数据的DOM都是依赖

vue2 中等粒度 用到数据的组件都是依赖

在getter中收集依赖 ,setter中触发依赖

Dep类和Watcher类

Dep 意味着dependence 依赖,它是专门用来管理依赖的一个类,每个Observer的实例,成员中都有一个Dep的实例

Watcher是中介类,当数据发生变化通过Watcher中转,通知组件

Watcher就是依赖,只有watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。

Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。

每一个Observer的实例中,都有一个dep实例

//像下面数据就会newObserver4次, 4个dep实例
let obj = {
    a:{
        b:{
            c:1
        }
    },
    g: [2,3]
}
//Dep类

//闭包全局uid
let uid = 0;
class Dep {
    constructor(){
        this.id = uid++;
        //subscribes 订阅者,数组储存自己的订阅者 ——即Watcher的实例,watcher是依赖
        this.subs = [];
    }
    addSub(sub){
        this.subs.push(sub);
    }
    depend(){
        if (Dep.target){
            this.addSub(Dep.target);
        }
    }
    notify(){
        //浅克隆
       	const subs = this.subs.slice();
        for (let i = 0,l = subs.length;i<l;i++){
            subs[i].update();
        }
    }
}
//Watcher类

var uid = 0;
export default class Watcher {
    constructor(target, expression, callback) {
        this.id = uid++;
        this.target = target;
        this.getter = parsePath(expression);
        this.callback = callback;
        this.value = this.get();
    }
    update() {
        this.run();
    }
    get() {
        // 进入依赖收集阶段。让全局的Dep.target设置为Watcher本身,那么就是进入依赖收集阶段
        Dep.target = this;
        const obj = this.target;
        var value;

        // 只要能找,就一直找
        try {
            value = this.getter(obj);
        } finally {
            Dep.target = null;
        }

        return value;
    }
    run() {
        this.getAndInvoke(this.callback);
    }
    getAndInvoke(cb) {
        const value = this.get();

        if (value !== this.value || typeof value == 'object') {
            const oldValue = this.value;
            this.value = value;
            cb.call(this.target, value, oldValue);
        }
    }
};

function parsePath(str) {
    var segments = str.split('.');

    return (obj) => {
        for (let i = 0; i < segments.length; i++) {
            if (!obj) return;
            obj = obj[segments[i]]
        }
        return obj;
    };
}

总结

vue2内部数据响应式实现的核心是Object.defineProperty(obj,key,{....}),对要监听的每个属性进行setter和getter的设置。

对于依赖收集这一块由于牵扯到了整个生命周期,目前还不是很清楚,现水平的理解是:

在每个vue实例化后(至于在什么时候实例化目前不清楚),会遍历vue实例的data,使用的是observe(data),这个时候就会进行一系列操作(后文细说),这个时候就已经对data中大部分的属性进行监控了(get,set),说是大部分是因为对数组则没有监控每一项,而是采用重写几个常用的方法来弥补监控(具体下文)。然后模板引擎进行渲染时候render,会touch这些数据(应该是执行new Watcher类似的操作),会将依赖(watcher实例)存储至各个Dep实例中去,当被监听的数据set触发时,会通知(notify)watcher进行相应的回调,处理完后就会重新渲染(后续diff等等)。

具体实现两部分:数据侦测和依赖收集

数据侦测是调用observe(obj),(如果不是对象直接返回)会进行Observer类的实例检测,如果有直接返回这个ob实例,没有则创建Observer实例并挂载到_ob _ 属性上(这个属性使用def工具函数声明不可枚举遍历)。创建Observer实例首先会new Dep,把dep实例挂载到ob实例的dep属性上,然后会分两种情况进行处理:

  1. 如果是对象调用walk遍历子属性,调用defineReactive。内部会先创建dep实例,然后再observe(val)——这个时候就完成了递归,对象的所有属性都会遍历到并且所有属性有dep实例,其中对象属性会有dep属性。接着就是一个拥有val值闭包的difineProperty函数:里面的get会进行依赖收集,调用dep.depend(),set调用dep.notify()并对新值也进行observe(newValue)
  2. 如果是数组则引入arrayMethods并把proto属性指向arrayMethods,然后对数组每项进行observe。其中arrayMethods的实现是基于AOP思想,不修改Array.prototype上的原型,只是拿过来进行改写:arrayMethods是Object.creat(Array.prototype)得来,然后对arrayMethods里面的push,pop,shift,unshift,slice,sort,reverse使用def工具函数进行改写def(arrayMethods,'method',function(){})。实现方法具体是 拿到原型方法,并apply调用得到结果最后返回(arguments的数组转换),内部则对会增添元素的方法进行额外的observeArray(addEleArray)。这个时候也会进行通知 dep.notify()进行处理。

依赖收集则依靠两个类:Dep和Watcher类

Dep类是管理依赖的类,每个监听的属性都有一个dep实例,里面存储了sub数组(watcher实例,订阅者),还有depend方法:存在一个全局变量Dep.target,用于储存当前的订阅者watcher,内部则是如果存在这个订阅者则将这个订阅者推入sub数组。notify方法:触发通知,通知自身所有订阅者(遍历sub数组),每个调用update方法

Watcher类是中介类,用于接收通知和主动触发收集依赖,它的构造函数需要传入要监听的对象,属性表达式,和属性修改触发的回调函数。关于属性表达式,就会有相对应的解析函数——split('.') 并遍历,最终返回值,parsePath这个函数是返回解析函数。在new Watcher类时候,会在构造函数中获取初始值,过程中就会调用这个parsePath,因为读取了属性值,就很自然的触发了属性的get函数,就进入了依赖收集(属性的dep将存储watcher实例),最后把value,cb等属性值存储在watcher实例中。dep触发的update方法则会储存旧值并把value重新get一次,最后call方法调用cb,传入新值和旧值。

一句话说:这两部分分别意味着:数据劫持和发布订阅模式的实现