乐趣区

关于vue.js:一文搞定数据响应式原理

关注公众号“执鸢者”,回复“材料”获取 500G 材料(各“兵种”均有),还有业余交换群等你一起来洒脱。(哈哈)

在 Vue 中,其中最最最外围的一个知识点就是数据响应式原理,数据响应式原理归纳起来就蕴含两大部分:侦测数据变动、依赖收集,理解这两个知识点就理解到了数据响应式原理的精髓。

一、侦测数据变动

可能帧听到数据变动是数据响应式原理的前提,因为数据响应式正是基于监听到数据变动起初触发一系列的更新操作。本次介绍数据响应式原理将基于 Vue2.x 进行,其将数据变为可被侦测数据时次要采纳了 Object.defineProperty()。

1.1 非数组对象

上面先举一个非数组对象的例子

const obj = {
    a: {
        m: {n: 5}
    },
    b: 10
};

察看下面的对象,能够发现其是存在蕴含关系的(即一个对象中可能蕴含另一个对象),那么天然会想到通过递归的形式实现,在 Vue 中为了保障代码较高的可读性,引入了三个模块实现该逻辑:observe、Observer、defineReactive,其调用关系如下所示:

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]);
        }
    }
}

二、依赖收集

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

其实看了这张神图后,因为能力无限还不是很了解,通过本人的拆分,认为能够分成两个步骤去了解。

  1. getter 中(Object.defineProperty 中的 get 属性)进行收集依赖后的状态

  1. 紧接着就是触发依赖,该过程是在 setter 中进行,当触发依赖时所存储在 Dep 中的所有 Watcher 均会被告诉并执行,告诉其关联的组件更新,例如数据更新的地位是与 Dep1 所关联的数据,则其上的 Watcher1、Watcher2、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. 关注公众号执鸢者,支付学习材料(前端“多兵种”材料),定期为你推送原创深度好文

退出移动版