Vue源码学习之响应式原理

前言

 响应式是指当数据发生变化后,Vue会通知到使用该数据的代码。当数据发生改变后也会通知视图进行改变,下面我们将简单的实现一下Vue中响应式原理的核心代码部分。

源码解析

Observer

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

export default class Observer {
    
    
    constructor(value) {
    
    
        // 每一个Observer的实例身上,都有一个dep
        this.dep = new Dep();
        // 给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)添加了__ob__属性,值是这次new的实例
        // 对__ob__每个属性使其无法进行枚举遍历,def这里是给予这个value添加__ob__并且无法进行枚举,
        // 并且其他属性也无法被修改  
        // 这个地方使得当前元素的__ob__是这个Observer类
        def(value, '__ob__', this, false);
        // console.log('我是Observer构造器', value);
        // 不要忘记初心,Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
        // 检查它是数组还是对象
        if (Array.isArray(value)) {
    
    
            // 如果是数组,要非常强行的蛮干:将这个数组的原型,指向arrayMethods
            // 修改其对应的原型为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]);
        }
    }
    
};

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

 1. 首先先对所传入Observer的对象设置一个属性为__ob__,这也就是我们平时在Vue打印输出变量对象时,可以看到其对象中是存在__ob__属性的,那么这个属性实际就是Observer实例。在代码中我们是通过def函数来去定义__ob__属性的,给予其进行劫持且enumerable为false,不可进行枚举。

2.判断所传入的对象是数组还是对象:
 如果是数组的话调用observeArray依次遍历数组的每一项都进行observe(这个方法我们在下面介绍,实际就是给数组的每一项都添加上Observer实例),对于数组来讲,我们还需要改写原型,对数组对象的七个方法进行改写,从而监听插入插入方法中所插入的数据,并对插入的数据进行数据监听,改变其数组对象的原型,设置为arrayMethods,此时arrayMethods是我们根据数组原型所进行改写的原型对象。
 如果是对象的话,则调用walk方法依次遍历对象的每一个值,并把每一个值都设置为响应式,这里调用了defineReactive方法,我们在后续进行详解。
 那么在Observer中所涉及的几个方法我们在下面进行赘述:

arrayMethods

 arrayMethods是改写了数组内部的方法,主要是监听进行插入元素的数组方法,并对所插入的元素使其也变为响应式的。对于非插入元素的数组方法,我们还是恢复其原来的功能即可,其实现代码如下:

// 得到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 = [];

        // 对插入新项的方法也要编程observe类型的
        switch (methodName) {
    
    
            case 'push':
            case 'unshift':
                inserted = args;
                break;
            case 'splice':
                // splice格式是splice(下标, 数量, 插入的新项)
                // slice所截取的是从下标为2一直往后,所以能够获取插入的新项
                inserted = args.slice(2);
                break;
        }

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

        return result;
    }, false);
});

 分析代码可得,首先先由数组对象的原型创建出了新的原型对象,然后我们列出数组的方法,并对这些方法进行重写,通过switch当遍历到到push、unshift、splice函数,并且splice函数所传入的参数为3个时(说明为插入项),则将inserted赋值为我们所插入的元素,如果判断inserted数组不为空,则重新遍历inserted的数组元素,并将其每一项都设置为响应式的。

observe

observe将其传入的对象进行通过Observer类进行实例化,那么通过观察之前Observe类的代码就可以发现,这里会存在递归调用,使得每一个子对象都进行Observer化。例如在Observer中当前对象为数组时会调用observer方法,在observer中发现其为对象又会调用Observer类进行实例化,这样就会使得每一个子对象都被observer化。


import Observer from './Observer.js';

// value是
export default function (value) {
    
    
    // 如果value不是对象,什么都不做
    if (typeof value != 'object') return;
    // 定义ob
    var ob;
    // __ob__是存储Observer实例的
    // 如果value身上有__ob__的话则直接赋值为ob并进行返回
    if (typeof value.__ob__ !== 'undefined') {
    
    
        ob = value.__ob__;
    } else {
    
    
        // 如果没有则进行初始化进行返回
        ob = new Observer(value);
    }
    return ob;
}

defineReactive

 为对象的属性添加响应式,进行数据劫持,我们还可以通过getter与setter来去进行一些操作,在后面我们会讲解到,在getter的时候就进行收集依赖,在setter函数里面我们就调用notify函数去通知dep进行进行更新,代码如下:

export default function defineReactive(data, key, val) {
    
    
    // 创建dep实例
    const dep = new Dep();
    // console.log('我是defineReactive', key);
    if (arguments.length == 2) {
    
    
        val = data[key];
    }

    // 子元素要进行observe,至此形成了递归。这个递归不是函数自己调用自己,而是多个函数、类循环调用
    let childOb = observe(val);

    Object.defineProperty(data, key, {
    
    
        // 可枚举
        enumerable: true,
        // 可以被配置,比如可以被delete
        // configurable特性表示对象的属性是否可以被删除,
        // 以及除value和writable特性外的其他特性是否可以被修改。
        configurable: true,
        // getter
        get() {
    
    
            console.log('你试图访问' + key + '属性');
            // 如果现在处于依赖收集阶段
            if (Dep.target) {
    
    
                dep.depend();
                if (childOb) {
    
    
                    childOb.dep.depend();
                } 
            }
            return val;
        },
        // setter
        set(newValue) {
    
    
            console.log('你试图改变' + key + '属性', newValue);
            if (val === newValue) {
    
    
                return;
            }
            val = newValue;
            // 当设置了新值,这个新值也要被observe
            childOb = observe(newValue);
            
            // 发布订阅模式,通知dep
            dep.notify();
        }
    });

};

Dep

 Dep是链接Observer和Watcher的桥梁,每一个Observer对应一个Dep,他内部维护一个数组,存放与该Obverser相关的Watcher
 对应实现代码:

// 闭包
var uid = 0;
// Dep是链接Observer和Watcher的桥梁,每一个Observer对应一个Dep,他内部维护一个数组,保护
// 与该Obverser相关的Watcher
export default class Dep {
    
    
    constructor() {
    
    
        console.log('我是DEP类的构造器');
        // 每创建一个就创建一个唯一的ID值
        this.id = uid++;

        // 用数组存储自己的订阅者。subs是英语subscribes订阅者的意思。
        // 这个数组里面放的是Watcher的实例

        // Watcher是我们所说的订阅者
        this.subs = [];
    }

    // 添加订阅
    addSub(sub) {
    
    
        // 将sub推入数组中
        this.subs.push(sub);
    }

    // 收集依赖
    // 添加依赖
    depend() {
    
    
        // Dep.target就是一个我们自己指定的全局的位置,你用window.target也行,只要是全剧唯一,没有歧义就行
        if (Dep.target) {
    
    
            // 就把相对应的watcher添加进来
            this.addSub(Dep.target);
        }
    }

    // 通知更新
    notify() {
    
    
        console.log('我是notify');
        // 浅克隆一份
        const subs = this.subs.slice();
        // 遍历
        for (let i = 0, l = subs.length; i < l; i++) {
    
    
            // 执行watcher的update函数,使其调用getAndInvoke,执行对应的回调函数进行更新操作
            subs[i].update();
        }
    }
};

 对于Dep它是链接Observer和Watcher的桥梁,当进行依赖收集时,我们就把对应的订阅者添加到Dep对象中,当Observer触发setter时,如果新值与旧值不同,那么则调用Dep实例的notify方法,通知实例中所有的订阅者(Watcher)进行调用update方法,通知组件进行更新。

Watcher

Watcher扮演的角色是订阅者/观察者,他的主要作用是为观察属性提供回调函数以及收集依赖(如计算属性computed,vue会把该属性所依赖数据的dep添加到自身的deps中),当被观察的值发生变化时,会接收到来自dep的通知,从而触发回调函数。
下面我们贴出实现代码:

import Dep from "./Dep";

var uid = 0;
// Watcher是一个订阅者身份,当监听的数据值修改时,
// 执行响应的回调函数,在Vue里面的更新模板内容
export default class Watcher {
    
    
    // target代表需要监听哪个对象
    // expression代表表达式,paresePath可以将其按.进行拆分
    // callback是代表其回调函数
    constructor(target, expression, callback) {
    
    
        console.log('我是Watcher类的构造器');
        this.id = uid++;
        this.target = target;

        this.getter = parsePath(expression);

        this.callback = callback;

        // 获取目标对象的value值
        this.value = this.get();
    }

    update() {
    
    
        this.run();
    }

    get() {
    
    
        // 进入依赖收集阶段。让全局的Dep.target设置为Watcher本身,那么就是进入依赖收集阶段
        Dep.target = this;

        // 获取传进来的目标对象
        const obj = this.target;

        var value;
        // 只要能找,就一直找
        try {
    
    
            // 获取对象根部的value值
            value = this.getter(obj);
        } finally {
    
    
            // 将全局Dep对象为null
            Dep.target = null;
        }

        return value;
    }

    run() {
    
    
        this.getAndInvoke(this.callback);
    }

    //得到并且唤起
    getAndInvoke(cb) {
    
    

        // 获取目标对象的value值
        const value = this.get();
        
        // 如果是值类型,如果之前的值不等于当前的值则执行
        // 如果为对象类型也执行对应的回调函数

        if (value !== this.value || typeof value == 'object') {
    
    
            const oldValue = this.value;
            this.value = value;
            // this指向设置为这个传入的对象,传参为newValue与oldValue
            // 执行更新操作
            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;
            // 比如传入 a.b.c.d
            // 这一步能逐渐剥得a:{b:{c:{d:55}}},下一步是b:{c:{d:55}},然后是c:{d:55}等等,直到可以获取目标值
            obj = obj[segments[i]]
        }
        return obj;
    };

}

 由代码分析可得,在Watcher类中expression代表我们逐步访问对象的层级属性值例如:“a.b.c.d”,代表我们需要访问a对象下面的d子对象,这个通过parsePath函数来进行解析,在调用Watcher起初我们就调用了get方法,get方法使得Dep.target设置为当前订阅者,然后调用了getter方法,当调用getter方法时,此时会触发defineReactive函数中defineProperty的数据劫持会使得触发get函数,在get函数中会判断Dep.target(某一个观察者)是否存在,如果存在的话则进行依赖收集,因为每一个被监听的数据都有一个Dep实例,那么将会把对应的Watcher添加到被监听数据(Observer)的Dep实例对象中进行依赖收集,一个数据会由多个订阅者进行订阅,统一放在Dep列表中。

总结

 Observe用于对数据进行监听,Dep是一个订阅器,存储多个订阅者,并通知实例中存储的订阅者进行更新。Watcher是订阅者/观察者,被Observe中Dep的实例对象进行存储,Observe中数据发生变化时,进行通知组件进行更新。
 当监听的数据进行取值操作时(getter),如果存在Dep.target(某一个观察者),则说明当前观察者(Watcher)是依赖该数据的,此时会把当前的观察者添加到Observer中Dep实例的subs数组中,等待后续数据变更的通知
 当监听的数据进行赋值操作时(setter),此时会触发Observer中Dep实例对象的notify方法,用于通知Dep实例对象中所有订阅者进行更新(通知组件进行更新)。
下面我们放一张图例,能够更好的说明三者之间的关系。
在这里插入图片描述
 本次分享的文章就到此结束啦,欢迎在讨论区一起交流~

Vue源码系列文章:

猜你喜欢

转载自blog.csdn.net/liu19721018/article/details/125435941