一文搞定数据响应式原理

关注公众号“执鸢者”,回复“资料”获取500G资料(各“兵种”均有),还有专业交流群等你一块儿来潇洒。(哈哈)前端

在Vue中,其中最最最核心的一个知识点就是数据响应式原理,数据响应式原理归结起来就包含两大部分:侦测数据变化、依赖收集,了解这两个知识点就了解到了数据响应式原理的精华。express

image.png

1、侦测数据变化

可以帧听到数据变化是数据响应式原理的前提,由于数据响应式正是基于监听到数据变化后来触发一系列的更新操做。本次介绍数据响应式原理将基于Vue2.x进行,其将数据变为可被侦测数据时主要采用了Object.defineProperty()。

1.1 非数组对象

下面先举一个非数组对象的例子
const obj = {
    a: {
        m: {
            n: 5
        }
    },
    b: 10
};
观察上面的对象,能够发现其是存在包含关系的(即一个对象中可能包含另外一个对象),那么天然会想到经过递归的方式实现,在Vue中为了保证代码较高的可读性,引入了三个模块实现该逻辑:observe、Observer、defineReactive,其调用关系以下所示:

image.png

1.1.1 observe

这个函数时帧听数据变化的入口文件,经过调用该函数一方面触发了其帧听对象数据变化的能力;另外一方面定义了什么时候递归到最内层的终止条件。
import Observer from './Observer';

export default function (value) {
    // 若是value不是对象,什么都不作(表示该递归到的是基本类型,其变化可被帧听的)
    if (typeof value !== 'object') {
        return;
    }

    // Observer实例
    let ob;
    // __ob__是value上的属性,其值就是对应的Observer实例(表示其已是可帧听的状态)
    if (typeof value.__ob__ !== 'undefined') {
        ob = value.__ob__;
    }
    else {
        // 是对象且该上属性仍是未可以帧听状态的
        ob = new Observer(value);
    }

    return ob;
}

1.1.2 Observer

这个函数的目的主要有两个:一个是将该实例挂载到该对象value的__ob__属性上(observe上用到了该属性,经过判断是否有该属性判断是否已经属于帧听状态);另外一个是遍历该对象上的全部属性,而后将该属性均变为可帧听的(经过调用defineReactive实现)。
export default class Observer {
    constructor(value) {
        // 给实例添加__ob__属性
        def(value, '__ob__', this, false);
        // 检查是数组仍是对象
        if (!Array.isArray(value)) {
            // 若为对象,则进行遍历,将其上的属性变为响应式的
            this.walk(value);
        }
    }

    // 对于对象上的属性进行遍历,将其变为响应式的
    walk(value) {
        for (let key in value) {
            defineReactive(value, key);
        }
    }
}

1.1.3 defineReactive

这个方法主要是将Object.defineProperty封装到一个函数中,作这一步操做的缘由是由于Object.defineProperty设置set属性时须要一个临时变量来存储变化前的值,经过封装利用闭包的思想引入val,这样就不须要在函数外面再设置临时变量了。
export default function defineReactive(data, key, val) {
    if (arguments.length === 2) {
        val = data[key];
    }

    // 子元素要进行observe,至此造成了递归
    let childOb = observe(val);

    Object.defineProperty(data, key, {
        // 可枚举
        enumerable: true,
        // 可配置
        configurable: true,
        // getter
        get() {
            console.log(`访问${key}属性`);
            return val;
        },
        // setter
        set(newValue) {
            console.log(`改变${key}的属性为${newValue}`);
            if (val === newValue) {
                return;
            }
            val = newValue;
            // 当设置了新值,这个新值也要被observe
            childOb = observe(newValue);
        }
    });
}

1.2 数组

Object.defineProperty不能直接监听数组内部的变化,那么数组内容变化应该怎么操做呢?Vue主要采用的是改装数组方法的方式(push、pop、shift、unshift、splice、sort、reverse),在保留其原有功能的前提下,将其新添加的项变为响应式的。
// array.js文件
// 获得Array的原型
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 => {
    //备份原来的方法
    const original = arrayMethods[methodName];
    // 定义新的方法
    def(arrayMethods, methodName, function () {
        // 恢复原来的功能
        const result = original.apply(this, arguments);

        // 将类数组对象转换为数组
        const args = [...arguments];
        // 数组不会是最外层,因此其上已经添加了Observer实例
        const ob = this.__ob__;

        // push/unshift/splice会插入新项,须要将插入的新项变成observe的
        let inserted = [];

        switch (methodName) {
            case 'push':
            case 'unshift': {
                inserted = args;
                break;
            }
            case 'splice': {
                inserted = args.slice(2);
                break;
            }
        }

        // 对于有插入项的,让新项变为响应的
        if (inserted.length) {
            ob.observeArray(inserted);
        }

        ob.dep.notify();

        return result;
    }, false);
});
除了改装其原有数组方法外,Observer函数中也将增长对数组的处理逻辑。
export default class Observer {
    constructor(value) {
        // 给实例添加__ob__属性
        def(value, '__ob__', this, false);
        // 检查是数组仍是对象
        if (Array.isArray(value)) {
            // 改变数组的原型为新改装的内容
            Object.setPrototypeOf(value, arrayMethods);
            // 让这个数组变为observe
            this.observeArray(value);
        }
        else {
            // 若为对象,则进行遍历,将其上的属性变为响应式的
            this.walk(value);
        }
    }

    // 对于对象上的属性进行遍历,将其变为响应式的
    walk(value) {
        for (let key in value) {
            defineReactive(value, key);
        }
    }

    // 数组的特殊遍历
    observeArray(arr) {
        for (let i = 0, l = arr.length; i < l; i++) {
            // 逐项进行observe
            observe(arr[i]);
        }
    }
}

2、依赖收集

目前对象中全部的属性已经变成可帧听状态,下一步就进入了依赖收集阶段,其整个流程以下所示:

image.png

其实看了这张神图后,因为能力有限还不是很理解,通过本身的拆分,认为能够分红两个步骤去理解。
  1. getter中(Object.defineProperty中的get属性)进行收集依赖后的状态

image.png

  1. 紧接着就是触发依赖,该过程是在setter中进行,当触发依赖时所存储在Dep中的全部Watcher均会被通知并执行,通知其关联的组件更新,例如数据更新的位置是与Dep1所关联的数据,则其上的Watcher一、Watcher二、WatcherN均会被通知并执行。
说了这么多,其中最核心的内容无外乎Dep类、Watcher类、defineReactive函数中的set和get函数。

2.1 Dep类

Dep类用于管理依赖,包含依赖的添加、删除、发送消息,是一个典型的观察者模式。
export default class Dep {
    constructor() {
        console.log('DEP构造器');
        // 数组存储本身的订阅者,这是Watcher实例
        this.subs = [];
    }

    // 添加订阅
    addSub(sub) {
        this.subs.push(sub);
    }

    // 添加依赖
    depend() {
        // Dep.target指定的全局的位置
        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();
        }
    }
}

2.2 Watcher类

Watcher类的实例就是依赖,在其实例化阶段会做为依赖存储到Dep中,在对应的数据改变时会更新与该数据相关的Watcher实例,进行对应任务的执行,更新对应组件。
export default class Watcher {
    constructor(target, expression, callback) {
        console.log('Watcher构造器');
        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;
        let 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) {
    const segments = str.split('.');

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

        return obj;
    };
}

2.3 defineReactive函数中的set和get函数

Object.defineProperty中的getter阶段进行收集依赖,setter阶段触发依赖。
export default function defineReactive(data, key, val) {
    const dep = new Dep();
    if (arguments.length === 2) {
        val = data[key];
    }

    // 子元素要进行observe,至此造成了递归
    let childOb = observe(val);

    Object.defineProperty(data, key, {
        // 可枚举
        enumerable: true,
        // 可配置
        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.notify();
        }
    });
}

参考文献

本文是笔者看了邵山欢老师的视频后作的一次总结,邵老师讲的真心很好,爆赞。

1.若是以为这篇文章还不错,来个分享、点赞吧,让更多的人也看到数组

2.关注公众号执鸢者,领取学习资料(前端“多兵种”资料),按期为你推送原创深度好文闭包

相关文章
相关标签/搜索