实现Vue-reactive功能

94 阅读5分钟

前言

说实话,看视频看的云里雾里,好像对于我这个vue的使用都没有很熟练的菜鸟来讲有些困难了😅,被50多分钟的视频硬控了快俩小时。但是好在自己反复对着代码查资料+问ai,也算是有点浅显的理解。

代码

废话不多说,直接上源码:

import { isObject } from '@vue/shared';

// 1.将数据转换成响应式数据,只能做对象类型数据的代理

const reactiveMap = new WeakMap(); //key只能是对象类型

// 实现同一个对象代理多次,返回同一个代理对象

// 代理对象的代理对象还是之前的代理对象

const enum ReactiveFlags { //响应式标识

    IS_REACTIVE = '__v_isReactive',

}

export function reactive(target) {

    if (!isObject(target)) {

        return target;

    }

    if (target[ReactiveFlags.IS_REACTIVE]) { //如果target是代理对象,那么它一定会在这一步中进入get

        return target; // 判定为代理对象,直接返回,对应多重代理问题

    }

    // 并没有重新定义属性,只是代理,在取值的时候会调用get,赋值时会调用set

    let existingProxy = reactiveMap.get(target);

    if (existingProxy) {

        return existingProxy;

    }

    // 第一次普通对象代理,会通过new Proxy代理一次

    // 下一次传入proxy对象,为了检测是否代理过,可以查看是否有get方法,有的话说明被proxy代理过

    const proxy = new Proxy(target, {

        get(target, key, receiver) {
        
            if (key === ReactiveFlags.IS_REACTIVE) {

                return true;

            }

            // 去代理对象上取值 使用get

            // return target[key]; 这种方式this指向有问题

            console.log(key);

            // 可以监控到用户取值

            const value = Reflect.get(target, key, receiver);

            // Proxy要配合Reflect使用,保证this指向正确

            // Reflect的recerver参数使this指向代理对象
            
            // 如果获取的值是对象,递归调用 reactive 函数将其转换为响应式对象
            
            if(isObject(value)){
                return reactive(value);
            }
            
            return value;

        },

        set(target, key, value, receiver) {

            // 去代理上设置值 使用set

            // target[key] = value;

            // 可以监控到用户设置值

            return Reflect.set(target, key, value, receiver);

        },

    });

    reactiveMap.set(target, proxy);

    return proxy;

}

自己的一点理解

  1. 首先,vue中的reactive大家都不陌生,它用于生成响应式数据,而且这个数据必须是对象类型。那么我们就先定义一个reactive函数,并规定参数只有一个并设置为对象(此处使用一个函数isObject()来判断)。
  2. 之后,我们使用Proxy来生成一个原数据target的代理(const proxy = new Proxy(target,{...}),自定义settergetter),此时有如下的代码:
const proxy = new Proxy(target, {

    get(target, key, receiver) {

        // 去代理对象上取值 使用get

        return target[key]; 

    },

    set(target, key, value, receiver) {

        // 去代理上设置值 使用set

        target[key] = value;

    },

});

但是问题来了,这样写的话return target[key];返回的依旧是原对象的键值,并没有被代理包裹。

  1. 那么我们想到了使用专门用来配合ProxyReflect,使用Reflect可以通过receiver参数指定this的指向,这里我们就可以让this始终指向代理对象: return Reflect.get(target, key, receiver),这样的话,当我们有如下代码:
function reactive(target) {

    if (isObject(target)) {

        return;

    }

    const proxy = new Proxy(target, {

        get(target, key, receiver) {

            console.log(key);

            return Reflect.get(target, key, receiver);

        },

        set(target, key, value, receiver) {

            return Reflect.set(target, key, value, receiver);

        },

    });

    return proxy;

}

// ----------------------------------------------------------

let target = {

    name: '123',

    get alias() {

        console.log(this);

        return this.name;

    },

};

let proxy = reactive(target);

proxy.alias;

控制台有如下输出:

Snipaste_2025-03-11_00-50-13.png

这说明我们已经实现了使用代理对象时this的正确指向。

  1. 现在问题又来了,如果我们将reactive对同一个对象使用两次,如:
const target = {
    name:'aaa',
    age:20
};
const A = reactive(target);
const B = reactive(target);

此时AB相同吗,答案显然是否定的,因为我们使用了new关键字,二者占用了不同的内存空间,这造成了浪费。那么我们需要判断一下是否该对象已经被代理过,我们自然想到了Map,但是WeakMap更适合此处,因为它可以更好的被垃圾回收机制管理,所以我们决定使用它创建一个存储原对象 : 代理的键值对集合,也就是源码中的reactiveMap。每次使用reactive()都进行如下操作:


......

let existingProxy = reactiveMap.get(target);

if (existingProxy) {

    return existingProxy;

}

......

reactiveMap.set(target, proxy);

......

这样我们就可以判断是否该对象已经被代理过,若代理过,则直接返回已有的代理,否则新建代理,避免了内存浪费。

  1. 另外,我们还想到一个特殊情况,假如一个对象被代理,然后对象的代理又被代理,如此嵌套,最终的结果怎么能保持只有一层Proxy代理?这时我们需要一个变量做标记:

......

const enum ReactiveFlags { //响应式标识

    IS_REACTIVE = '__v_isReactive',

}

......

if (target[ReactiveFlags.IS_REACTIVE]) {

    //如果target是代理对象,那么它一定会在这一步中进入get

    return target; // 判定为代理对象,直接返回,对应多重代理问题

}

......

const proxy = new Proxy(target, {

    get(target, key, receiver) {

        if (key === ReactiveFlags.IS_REACTIVE) {

            return true;

        }

        return Reflect.get(target, key, receiver);

    },

    set(target, key, value, receiver) {

    return Reflect.set(target, key, value, receiver);

    },

});

......

因为Proxy在调用属性时一定会经过get,所以可以利用这一点进行判断。如果target是普通对象,那么在经过if语句时会跳过执行后方的正常流程,通过new来生成代理对象;而如果它本来就是代理对象,则会则if这一步因为读取ReactiveFlags.IS_REACTIVE键值而跳转到get内部,此时key等于该值,所以if语句为真,直接返回本就是代理对象的target

结语

到这里就结束了,希望能对自己的代码水平有提高,之后也会继续学习。如果对您有帮助,欢迎帮我点赞,有问题也欢迎指正,虚心求教🙏。