Vue源码探秘之数据响应式原理

一、Object.defineProperty()方法

彻底弄懂Vue2的数据更新原理,说白了,就是弄清楚MVVM是怎么实现的,手写相关实现代码,让相关知识不再处于“忽悠阶段”。

image.png

1.1 从MVVM模式说开去

image.png

1.2 侵入式和非侵入式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AEdTLZgZ-1656422931911)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9114e005f29743dd8a5fbe7b427cf83a~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image?)]

1.3 尤雨溪发现了“上帝的钥匙”

Object.defineProperty()是“上帝的钥匙”,可以做 数据劫持/ 数据代理;
Object.defineProperty()其实是利用JavaScript引擎赋予的功能, 检测对象属性变化;
仅有“上帝的钥匙” 不够, 还需要设计一套精密的系统;

1.4 Object.defineProperty()方法———定义及更新对象属性

Object.defineProperty()方法会直接在一个对象上定义一个新属性, 或者修改一个对象的现有属性, 并返回此对象;
思考:为什么要通过Object.defineProperty()来定义,或者,为什么javascript要提供这么一个方法,直接obj.a=3不香吗?因为这样,我们就可以为该对象设置一些额外隐藏的属性。
//可延续使用上一篇【# Vue源码探秘之虚拟DOM和diff算法】,用webpack搭建好的项目环境
// src/index.js
var obj = {};
// 这样就可以给对象定义一个属性了,那么,思考:为什么要这么定义,直接obj.a=3不香吗
// 因为这样定义出来的属性有响应式???
Object.defineProperty(obj, 'a', {
    value: 3
});
Object.defineProperty(obj, 'b', {
    value: 5
});
console.log(obj);
console.log(obj.a, obj.b); 

1.5 Object.defineProperty()方法———设置额外隐藏的属性

Object.defineProperty()方法可以设置一些额外隐藏的属性;
比如:get、set、value、writable、enumerable、configurable
var obj = {};
Object.defineProperty(obj, 'a', {
    value: 3,
    // 是否可写
    writable: false
});
Object.defineProperty(obj, 'b', {
value: 5,
// 是否可以被枚举
enumerable: false
});

console.log(a++);//输出还是3

for(var key in obj) {
    //在b中,enumerable: false时,只有a属性被输出,没有b,b会被跳过
    console.log(k);
} 

1.6 大名术语读的时候getter/setter,写的时候是get/set

getter get() 数据劫持,一旦某属性被访问了,就会无条件的执行其对应getter函数
setter set() 数据代理,一旦某属性的值被改变/设置了,就会无条件的执行其对应setter函数

image.png

var obj = {};
Object.defineProperty(obj, 'a', {
    // getter  数据劫持,一旦a属性被访问了,就会无条件的执行getter函数
    get() {
        console.log('你试图访问obj的a属性');
    },
    // setter  数据代理,一旦a的值被改变/设置了,就会无条件的执行setter函数
    set() {
        console.log('你试图改变obj的a属性');
    }
});

obj.a = 10;
console.log(obj.a);//此时还是undefined,因为你在set()里面什么也没没干 

二、defineReactive函数

为什么要定义一个defineReactive函数???

2.1 getter/setter需要变量周转才能工作

var obj = {};
var temp;
Object.defineProperty(obj, 'a', {
    // getter
    get() {
        console.log('你试图访问obj的a属性');
        return temp;
    },
    // setter
    set(newValue) {
        console.log('你试图改变obj的a属性', newValue);
        temp = newValue;
    }
});
console.log(obj.a);
obj.a++;
console.log(obj.a); 

2.2 使用defineReactive函数不需要设置临时变量了, 而是用闭包

function defineReactive(data, key, val) {
    Object.defineProperty(data, key, {
        // 可枚举
        enumerable: true,
        // 可以被配置, 比如可以被delete
        configurable: true,
        // getter
        get() {
            console.log('你试图访问'+ data + '的' + key + '属性');
            return val;
        },
        // setter
        set(newValue) {
            console.log('你试图改变'+ data + '的' + key + '属性', newValue);
            if (val === newValue) {
                return;
            }
            val = newValue;
        }
    });
};
defineReactive(obj, 'a', 666)
console.log(obj.a);//666
obj.a = 10;
obj.a++;
console.log(obj.a);//11 

三、递归侦测对象全部属性

image.png

// src/index.js
import observe from './observe.js';
import Watcher from './Watcher.js';

var obj = {
    a: {
        m: {
            n: 5
        }
    },
    b: 10,
    c: {
        d: {
            e: {
                f: 6666
            }
        }
    },
    g: [22, 33, 44, 55]
};


observe(obj);
new Watcher(obj, 'a.m.n', (val) => {
    console.log('★我是watcher,我在监控a.m.n', val);
});
obj.a.m.n = 88;
obj.g.push(66);//此时可得知g被访问,但是66这个新增的值这件事没有被检测到,即此时
//的数组不是响应式的
console.log(obj); 
// src/observe.js
import Observer from './Observer.js';
export default function (value) {
    // 如果value不是对象,什么都不做
    if (typeof value != 'object') return;
    // 定义ob
    var ob;
    if (typeof value.__ob__ !== 'undefined') {
        ob = value.__ob__;
    } else {
        ob = new Observer(value);
    }
    return ob;
} 
// src/Observer.js
import { def } from './utils.js';
import defineReactive from './defineReactive.js';
import { arrayMethods } from './array.js';
import observe from './observe.js';
import Dep from './Dep.js';

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]);
        }
    }
}; 
// src/utils.js
export const def = function (obj, key, value, enumerable) {
    Object.defineProperty(obj, key, {
        value,
        enumerable,
        writable: true,
        configurable: true
    });
}; 
// src/defineReactive.js
import observe from './observe.js';
import Dep from './Dep.js';

export default function defineReactive(data, key, val) {
    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: 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();
        }
    });
}; 

四、数组的响应式处理

4.1 setPrototypeOf 与 Object.create区别

设置原型的目的是进行对象间的委托,可以让一个对象获得另一个对象的一些属性或者方法。关于设置一个对象的原型,JS提供了俩种方式。一种是通过setPrototypeOf,另一种是Object.create,通过了解二者的区别可以让我们能够根据情况去选择适合的方式。 

用法

若存在A和B俩个函数,让A的原型指向B 

setPrototypeOf

Object.setPrototypeOf(A.prototype,B.prototype) 

Create

A.prototype = Object.create(B.prototype) 

区别

使用Object.create,A.prototype将会指向一个空对象,空对象的原型属性指向B的prototytpe。所以我们不能再访问A的原有prototypoe中的属性。Object.create的使用方式也凸显了直接重新赋值。

使用Object.setPrototypeOf则会将A.prototype将会指向A原有的prototype,然后这个prototype的prototype再指向B的prototytpe。所以我们优先访问的A,然后再是B。

在进行俩个原型之间的委托时使用setPrototypeOf更好,Object.create更适和直接对一个无原生原型的对象快速进行委托。 

4.2 改写数组的七个方法

到目前为止,以上的代码,访问数组时可以被检测到,但是改了数组,却没有得到检测,那么,如何实现对于数组的响应式,实际上,vue底层改写了数组的七大方法以实现响应式,七大方法为:push pop shift unshift splice sort reverse
这七个方法都是Array.prototype上的方法,现在我们就是要备份这个七个方法,在此基础上再添加一些新方法、或者重写。
在Observer.js里使用Object.setPrototypeOf(value,arrayMethods)
在array.js里使用arrayMethods = Object.create(Array.prototype)

image.png

// src/array.js
import { def } from './utils.js';

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

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

// 要被改写的7个数组方法,其中push,unshift,splice很特别
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) {
        // 原型上有observeArray这个方法
            ob.observeArray(inserted);
        }

        console.log('啦啦啦');

        ob.dep.notify();

        return result;
    }, false);
}); 

五、依赖收集

5.1 什么是依赖?

需要用到数据的地方, 称为依赖

Vue1.x, 细粒度依赖, 用到数据的DOM都是依赖;
Vue2.x, 中等粒度依赖, 用到数据的组件是依赖;
在getter中收集依赖, 在setter中触发依赖

5.2 Dep类和Watcher类

把依赖收集的代码封装成一个Dep类, 它专门用来管理依赖, 每个Observer的实例,成员中都有一个Dep的实例;
Watcher是一个中介, 数据发生变化时通过Watcher中转, 通知组件;

image.png

依赖就是Watcher。 只有Watcher触发的getter才会收集依赖, 哪个Watcher触发了getter, 就把哪个Watcher收集到Dep中。
Dep使用发布订阅模式, 当数据发生变化时, 会循环依赖列表, 把所有的Watcher都通知一遍。
代码实现的巧妙之处: Watcher把自己设置到全局的一个指定位置,然后读取数据, 因为读取了数据, 所以会触发这个数据的getter。 在getter中就能得到当前正在读取数据的Watcher, 并把这个Watcher收集到Dep中。

image.png

// src/Dep.js
var uid = 0;
export default class Dep {
    constructor() {
        console.log('我是DEP类的构造器');
        this.id = uid++;

        // 用数组存储自己的订阅者。subs是英语subscribes订阅者的意思。
        // 这个数组里面放的是Watcher的实例
        this.subs = [];
    }
    // 添加订阅
    addSub(sub) {
        this.subs.push(sub);
    }
    // 添加依赖
    depend() {
        // Dep.target就是一个我们自己指定的全局的位置,你用window.target也行,只要是全剧唯一,没有歧义就行
        if (Dep.target) {
            this.addSub(Dep.target);
        }
    }
    // 通知更新
    notify() {
        console.log('我是notify');
        // 浅克隆一份
        const subs = this.subs.slice();
        // 遍历
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update();
        }
    }
}; 
// src/Watcher.js
import Dep from "./Dep";

var uid = 0;
export default class Watcher {
    constructor(target, expression, callback) {
        console.log('我是Watcher类的构造器');
        this.id = uid++;
        this.target = target;
        // 实现表达式a.b.c按点拆分
        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;
    };
} 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue 2 和 Vue 3 在数据响应式原理上有一些区别。以下是它们之间的主要区别: 1. Reactivity API(响应式 API): - Vue 2:Vue 2 使用 Object.defineProperty() 来追踪属性的变化,并通过 getter 和 setter 来劫持属性的访问和修改,从而实现数据响应式。 - Vue 3:Vue 3 引入了一个新的响应式系统,使用 Proxy 对象来实现数据响应式。Proxy 可以拦截对象上的各种操作,包括属性的读取、设置、删除等。 2. 引入了 Proxy 对象: - Vue 2:Vue 2 没有使用 Proxy 对象。 - Vue 3:Vue 3 使用 Proxy 对象来代替 Vue 2 的 Object.defineProperty()。Proxy 具有更强大和灵活的功能,可以捕获更多类型的操作,并且可以直接监听整个对象或数组,而不需要遍历每个属性。 3. 响应式侦听(Reactivity Tracking): - Vue 2:Vue 2 使用递归遍历来追踪数据的变化,这意味着在大型对象或数组上可能会有性能问题。 - Vue 3:Vue 3 使用了基于依赖图的跟踪机制,只追踪实际使用的属性,而不是整个对象。这样可以提高性能并减少不必要的侦听。 4. 静态树优化(Static Tree Optimization): - Vue 2:Vue 2 的虚拟 DOM 对比算法是基于深度优先遍历的,无法识别静态子树,导致在重新渲染时可能会重复创建和销毁组件。 - Vue 3:Vue 3 引入了静态树优化,通过标记和提升静态节点,可以跳过对它们的对比和渲染过程,从而提高性能。 总的来说,Vue 3 在数据响应式上采用了 Proxy 对象和基于依赖图的跟踪机制,提供了更强大和高效的响应式系统,并引入了一些优化技术来提高性能。这些改进使得 Vue 3 在处理大型应用程序和复杂数据结构时更加高效和灵活。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值