手写Vue3源码(实现一个mini-vue) ---实现Reactive

576 阅读11分钟

2024-7-5

关于Reactive

官方介绍:

reactive ()

返回一个对象的响应式代理。

  • 类型
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
  • 详细信息
  • 响应式转换是“深层”的:它会影响到所有嵌套的属性。一个响应式对象也将深层地解包任何 ref 属性,同时保持响应性。
  • 值得注意的是,当访问到某个响应式数组或 Map 这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包。
  • 若要避免深层响应式转换,只想保留对这个对象顶层次访问的响应性,请使用 shallowReactive() 作替代。
  • 返回的对象以及其中嵌套的对象都会通过 ES Proxy 包裹,因此不等于源对象,建议只使用响应式代理,避免使用原始对象。

实现Reactive

从官方描述来看,Reactive本质上就是将对象利用Proxy进行代理,生成一个代理对象,从而实现响应式。

话不多说,先实现一个简单的代理对象吧!

1.简单的代理对象

// reactive 方法
export function reactive(target) {
    // 调用创建响应式对象方法 统一处理
    return createReactiveObject(target)
}

// 代理逻辑
const mutableHandlers:ProxyHandler<any>= {
    // 读取对象属性时的回调方法
    get(target,key,receiver){    //receiver 指向代理后的对象   
    },
    // 设置对象属性时的回调方法
    set(target,key,value,receiver){     //receiver 指向代理后的对象
        return true
    }
}
// 创建响应式对象方法
function createReactiveObject(target){
    // * * * * * * * * *
    // 判断当前是否为对象
    // 若不是对象 直接返回 不做响应式
    // 例:reactive(123) => 123
    if (!isObject(target)) {
        return target
    }
    // * * * * * * * * *
    
    // 将当前对象 利用Proxy方法 做代理
    let proxy = new Proxy(target,mutableHandlers)
    return proxy
}

此时,一个简单的reactive方法就实现了,我们可以利用此方法创建一个Proxy对象。

     // 功能实现:
     // 1、只对 对象类型 做响应式
     let obj = reactive(123)
     console.log(obj);  // 123
     let obj1 = reactive({name:'ywh'})
     console.log(obj);   // 此时obj为Proxy对象

2.避免重复代理

刚才实现的是最基本的reactive方法,但我们需要考虑一种情况———重复代理。

也就是将同一对象做多次代理。

let obj = {name:'ywh'}
let proxyObj1 = reactive(obj)
let proxyObj2 = reactive(obj)

重复代理,是一件浪费性能的事情,而且也没有必要。

那该如何避免重复代理呢?

一种思路是,将当前对象与其代理对象建立映射关系,并且缓存起来。

每次创建代理对象时,先去缓存表里看看是否有当前对象,若有,说明已经缓存过,即当前对象已经代理过了,那此时就无需再次代理,直接将对应缓存的代理对象返回即可。

// 缓存 对象 与其 代理对象 的映射
// 用于 避免重复创建代理对象
const reactiveMap =new WeakMap()

export function reactive(target) {
    // 调用创建响应式对象方法 统一处理
    return createReactiveObject(target)
}

// 代理逻辑
const mutableHandlers:ProxyHandler<any>= {
    // 读取对象属性时的回调方法
    get(target,key,receiver){    //receiver 指向代理后的对象
        if (key ===ReactiveFlags.IS_REACTIVE ) {
            return true
        }       
    },
    // 设置对象属性时的回调方法
    set(target,key,value,receiver){     //receiver 指向代理后的对象
        return true
    }
}

// 创建响应式对象方法
function createReactiveObject(target){
    // 判断当前是否为对象
    // 若不是对象 直接返回 不做响应式
    // 例:reactive(123) => 123
    if (!isObject(target)) {
        return target
    }
    
    // * * * * * * * * *
    // 新增逻辑:
    // 判断当前对象 是否已经代理过
    // 即:判断当前对象是否在reactiveMap缓存过
    const exitsProxy = reactiveMap.get(target)
    if (exitsProxy) {
        // 若缓存过
        // 返回其代理对象
        return exitsProxy
    }
    // * * * * * * * * *

    // 若是对象 则对其做代理
    // 普通对象 => 代理对象
    // target:目标对象 mutableHandlers:代理逻辑
    let proxy = new Proxy(target,mutableHandlers)

    // 代理完成后 
    // 将当前对象 及其 代理 缓存
    reactiveMap.set(target,proxy)

    return proxy
}

从而,我们可以实现避免重复代理功能

// 功能实现:
]// 3、实现缓存 避免重复代理
let obj = {name:'ywh'}
let proxyObj1 = reactive(obj)
let proxyObj2 = reactive(obj)
console.log(proxyObj1 === proxyObj2); // true

3.避免嵌套代理

除了重复代理(同一对象多次代理)的情况,我们也还需要考虑代理对象再次代理的套娃情况,即嵌套代理。

这也就意味着,我们需要识别出当前对象是代理对象,还是未代理的普通对象。

根据我们当前已写的逻辑,代理对象只不过是利用Proxy方法,将普通对象做一层代理,当我读取或者修改此对象属性时,触发get和set方法。不过如此~~~

既然如此,那我们是不是可以在代理对象的get方法里做文章??

对于普通对象而言,访问其属性,就只是单纯在其身上寻找该属性;而代理对象,访问其属性,会触发其get方法。

那么我可以设计一个全局唯一的属性名!

普通对象访问此属性时,值一定为空,布尔值类型为false;

而代理对象访问此属性,一定会进入get方法,我可以在get方法里做个判断,若当前访问的是全局唯一的属性名,则返回布尔值true。

// * * * * * * * * * * * * 
// 新增逻辑:
// 设计一个全局唯一的属性名
// 代理标记
enum ReactiveFlags{
    IS_REACTIVE="__v_isReactive",
}
// * * * * * * * * * * * * 

export function reactive(target) {
    // 调用创建响应式对象方法 统一处理
    return createReactiveObject(target)
}

// 代理逻辑
const mutableHandlers:ProxyHandler<any>= {
    // 读取对象属性时的回调方法
    get(target,key,receiver){    //receiver 指向代理后的对象
        if (key ===ReactiveFlags.IS_REACTIVE ) {
            return true
        }       
    },
    // 设置对象属性时的回调方法
    set(target,key,value,receiver){     //receiver 指向代理后的对象
        return true
    }
}

// 缓存 对象 与其 代理对象 的映射
// 用于 避免重复创建代理对象
const reactiveMap =new WeakMap()

// 创建响应式对象方法
function createReactiveObject(target){
    // * * *
    // 判断当前是否为对象
    // 若不是对象 直接返回 不做响应式
    // 例:reactive(123) => 123
    if (!isObject(target)) {
        return target
    }
    
    // * * * * * * * * * * * * 
    // 新增逻辑:
    // 判断当前对象是否为代理对象
    // 普通对象一定为null 而代理对象则会进入get回调函数内
    // 此时可以在get回调函数内 处理相关逻辑
    if (target[ReactiveFlags.IS_REACTIVE]) {
        // 若当前对象为代理对象
        // 则直接返回  不必再做代理
        return target
    }
    // * * * * * * * * * * * * 

    // 判断当前对象 是否已经代理过
    // 即:判断当前对象是否在reactiveMap缓存过
    const exitsProxy = reactiveMap.get(target)
    if (exitsProxy) {
        // 若缓存过
        // 返回其代理对象
        return exitsProxy
    }

    // 若是对象 则对其做代理
    // 普通对象 => 代理对象
    // target:目标对象 mutableHandlers:代理逻辑
    let proxy = new Proxy(target,mutableHandlers)

    // 代理完成后 
    // 将当前对象 及其 代理 缓存
    reactiveMap.set(target,proxy)

    return proxy
}

创建一个全局唯一的属性,并且利用代理对象的性质,从而可以避免嵌套代理。

        // 功能实现:
        // 4、避免嵌套代理
        let proxyObj = reactive({name:'ywh'})
        let ProxyAgainObj = reactive(proxyObj)
        console.log(proxyObj === ProxyAgainObj);    // true

此时,当前文件略显臃肿,我们可以将代理逻辑以及代理标记抽出去,方便后续增加新方法。

创建baseHandler.ts文件

//baseHandler.ts

// 代理标记
export enum ReactiveFlags{
    IS_REACTIVE="__v_isReactive",
}
// 代理逻辑
export const mutableHandlers:ProxyHandler<any>= {
    // 读取对象属性时的回调方法
    get(target,key,receiver){    //receiver 指向代理后的对象
        if (key ===ReactiveFlags.IS_REACTIVE ) {
            return true
        }       
    },
    // 设置对象属性时的回调方法
    set(target,key,value,receiver){     //receiver 指向代理后的对象
        return true
    }
}

4.实现代理逻辑

实现代理逻辑的get和set方法

一种很自然的想法是:

get()方法返回对象的key属性,即target[key] ,

set()方法设置对象属性为新传入的value值,即target[key]=value

// 代理逻辑
export const mutableHandlers:ProxyHandler<any>= {
    // 读取对象属性时的回调方法
    get(target,key,receiver){    //receiver 指向代理后的对象
        if (key ===ReactiveFlags.IS_REACTIVE ) {
            return true
        }    
        return target[key]   
    },
    // 设置对象属性时的回调方法
    set(target,key,value,receiver){     //receiver 指向代理后的对象
         target[key]=value
         return true
    }
}

但这样会存在一个问题!!!

例如如下代码:

// 创建一个person对象 
// 对象内有一个name属性和一个getter方法
const person ={
    name:'ywh',
    get outputName(){
        return this.name + 'is good man!'
    }
}

// 给person对象做代理
// proxyPerson 即为person的代理对象
// 内部有一个name属性和一个getter方法
// 同时 给他们都配置了get回调
const proxyPerson = new Proxy(person,{
    get(target,key,receiver){
        console.log(key);
        return target[key]
    }
})

// 此时 访问proxyPerson代理对象的outputName方法
// 程序执行流程:
// =>    触发get方法
// =>    Output: outputName 输出key
// =>    读取原对象上的outputName属性
// =>    此时 此方法this指向person对象 
// =>    Output: ywh is good man! 输出
console.log(proxyPerson.outputName);

看起来,好像没问题?

确实输出了想要的结果。

但是仔细观察会发现,执行outputName方法时,访问了person对象的name属性,但是name属性并没有触发get方法。

原因在于outputName方法中的this指向的是person对象,而非代理对象proxyPerson,从而不会触发get方法。

这有很大的问题!!

这意味着,当我后续修改name属性时,不会触发proxyPerson.outputName依赖的effect,会丢失响应式!!

那我们尝试修改代码,返回代理对象proxyPerson身上的outputName方法,而非原对象身上的outputName方法。

// 创建一个person对象 
// 对象内有一个name属性和一个getter方法
const person ={
    name:'ywh',
    get outputName(){
        return this.name + 'is good man!'
    }
}

// 给person对象做代理
// proxyPerson 即为person的代理对象
// 内部有一个name属性和一个getter方法
// 同时 给他们都配置了get回调
const proxyPerson = new Proxy(person,{
    get(target,key,receiver){
        console.log(key);
        return receiver[key]
    }
})

// 此时 访问proxyPerson代理对象的outputName方法
// 程序执行流程:
// =>    触发get方法
// =>    Output: outputName 输出key
// =>    读取代理对象上的outputName属性
// =>    触发get方法
// =>    Output: outputName 输出key
// =>    读取代理对象上的outputName属性
// =>    触发get方法
// =>    Output: outputName 输出key
// =>    读取代理对象上的outputName属性
// =>    ......
console.log(proxyPerson.outputName);

糟糕!!!

我们发现程序加入了死循环!!!

每次读取代理对象上的outputName属性时,都会触发get方法;

而get方法又会读取代理对象上的outputName属性。

从而陷入了死循环 T^T.

我们尝试了从原对象身上找属性(会导致丢失响应式),以及从代理对象身上找属性(会陷入死循环),结果都不如意。

那我们想想,能不能够还是从原对象身上找属性,但是修改属性内的this为代理对象呢?

我们试试看!

利用Reactive.get()方法,实现从原对象中读取属性,但修改this为代理对象。

来自MDN介绍:

Reflect.get() 方法与从对象 (target[propertyKey]) 中读取属性类似,但它是通过一个函数执行来操作的。

参数

target: 需要取值的目标对象

propertyKey: 需要获取的值的键值

receiver: 如果target对象中指定了getter,receiver则为getter调用时的this值。

// 创建一个person对象 
// 对象内有一个name属性和一个getter方法
const person ={
    name:'ywh',
    get outputName(){
        return this.name + 'is good man!'
    }
}

// 给person对象做代理
// proxyPerson 即为person的代理对象
// 内部有一个name属性和一个getter方法
// 同时 给他们都配置了get回调
const proxyPerson = new Proxy(person,{
    get(target,key,receiver){
        console.log(key);
        return Reflect.get(target,key,receiver)
    }
})

// 此时 访问proxyPerson代理对象的outputName方法
// 程序执行流程:
// =>    触发get方法
// =>    Output: outputName 输出key
// =>    读取原对象上的outputName属性 修改其中this为代理对象
// =>    执行原对象上的outputName方法
// =>    读取this.name  
// =>    触发get方法
// =>    Output: name 输出key
// =>    返回原对象的name属性值
// =>    返回 ywh is good man!
// =>    Output:ywh is good man! 输出最终结果
console.log(proxyPerson.outputName);

此时,发现outputName和name都触发了get方法,从而实现了响应式!!!

We get it ~~~

所以此时修改一下代理逻辑的get和set方法

// 代理标记
export enum ReactiveFlags{
    IS_REACTIVE="__v_isReactive",
}
// 代理逻辑
export const mutableHandlers:ProxyHandler<any>= {
    // 读取对象属性时的回调方法
    get(target,key,receiver){    //receiver 指向代理后的对象
        if (key ===ReactiveFlags.IS_REACTIVE ) {
            return true
        }    
        // Reflect作用 => 用于修改this指向
        // 若target[key] 是一个getter函数
        // 可以让其中的this 指向receiver
        return Reflect.get(target,key,receiver)    
    },
    // 设置对象属性时的回调方法
    set(target,key,value,receiver){     //receiver 指向代理后的对象
        // Reflect作用 => 用于修改this指向
        // 若target[key] 是一个setter函数
        // 可以让其中的this 指向receiver
        return Reflect.set(target,key,value,receiver)
    }
}

从而,我们实现了代理逻辑 get 和 set

        // 功能实现:
        // 5、触发代理逻辑 get 和 set
        let proxyObj = reactive({ name: 'ywh' })
        console.log(proxyObj.name);     // 此时会调用get方法

至此!!

我们实现了Reactive方法,可以实现将普通对象进行代理,从而可以方便后续实现响应式。