从0到1实现自己的Mini-Vue3(3)

337 阅读5分钟

这一次让我们一起来实现readonly、isReactive、isReadonly、isProxy、嵌套对象的reactive、shallowReadonly功能

1.实现readonly、isReactive、isReadonly、isProxy

先看如下用例,在src/reactivity/test/目录下增加readonly.spec.ts文件,存放readonly相关单元测试

import { isProxy, isReadonly, readonly } from '../reactive';

describe('readonly',() => {
    it("happy path",() => {
        const original = {foo:1}
        const wrapped = readonly(original)
        expect(wrapped).not.toBe(original)
        expect(isReadonly(wrapped)).toBe(true)
        expect(isProxy(wrapped)).toBe(true)
        expect(isReadonly(original)).toBe(false)
        expect(wrapped.foo).toBe(1)
    })

    it('warn then call set',() => {
        console.warn = jest.fn()
        const user = readonly({
            age:10
        })
        user.age = 11
        expect(console.warn).toBeCalledTimes(1)
    })
})

同样在reactive.spec.ts中增加isReactive相关用例

- import { reactive } from '../reactive';
+ import { isProxy, isReactive, reactive } from '../reactive';
it("happy path",() => {
    const original = {foo:1}
    const observed = reactive(original)
    expect(observed).not.toBe(original)
    expect(observed.foo).toBe(1)
+   expect(isReactive(observed)).toBe(true)
+   expect(isProxy(observed)).toBe(true)
+   expect(isReactive(original)).toBe(false)
})

看完上面的用例,相信你对于readonly、isReactive、isReadonly、isProxy这几个API已经有大概的认识了。

没错,readonly是用来声明一个只读的对象,并且在修改时或有warn提示,isReactive和isReadonly分别用来判断对象是否是经过reactive或者readonly处理后返回的,isProxy判断对象是isReactive或者isReadonly的

首先,我们可以尝试实现readonly,类似reactive,我们也是用Proxy对原对象raw进行处理,只不过不同的是,在set部分我们使用console.warn给出一段提示。话不多少,上代码。

在reactive.ts中增加readonly方法,其实是把reactive方法的代码拷过来,对set稍作修改

+ export function readonly (raw) {
+     return new Proxy(raw, {
+         get(target, key) {
+             track(target, key)
+             return Reflect.get(target, key)
+         },
+         set(target, key, value) {
+             console.warn(`key:${key}是只读的,不可修改`)
+             return true
+         }
+     })
+ }

执行yarn test readonly测试之后,发现第二个用例已经通过

image.png

然后,来看第一个用例,也就是isReactive、isReadonly和isProxy,当然其实实现了前两个,最后一个也就完成了。

观察到reactive和readonly方法现在的代码存在重复,考虑封装成一个统一的方法,另外也对readonly的get进行完善

由于readonly不可修改,所以也就不存在依赖收集的工作,所以这里我们创建一个高阶函数createGetter,用来返回get函数

修改reactive.ts如下

+ function createSetter() {
+     return function set(target, key, value) {
+         let res = Reflect.set(target, key, value)
+         trigger(target, key)
+         return res
+     }
+ }
+ // 创建高阶函数根据参数isReadonly,来创建不同类型的get函数
+ function createGetter(isReadonly = false) {
+     return function get(target, key) {
+         const res = Reflect.get(target, key)
+           if (!isReadonly) {
+               track(target, key)
+           }
+           return res
+     }
+ 
+ }
+ const get = createGetter()
+ const set = createSetter()
+ const readonlyGet = createGetter(true)
export const reactive = (raw) => {
    return new Proxy(raw, {
-        get (target, key) {
-            track(target, key)
-            return Reflect.get(target, key)
-        },
+        get,
-        set (target, key, value) {
-            let res = Reflect.set(target, key, value)
-            trigger(target, key)
-            return res
-        }
+        set
    })
}
export function readonly(raw) {
    return new Proxy(raw, {
-        get (target, key) {
-            track(target, key)
-            return Reflect.get(target, key)
-        },
+       get: readonlyGet,
        set(target, key, value) {
            console.warn(`key:${key}是只读的,不可修改`)
            return true
        }
    })
}

考虑到reactive和readonly的Proxy的handler比较类似,源码中进行了进一步的封装 在reactive.ts的同级目录增加baseHandlers.ts文件,存放Proxy handler相关内容

import { track, trigger } from './effect'

function createSetter() {
    return function set(target, key, value) {
        let res = Reflect.set(target, key, value)
        trigger(target, key)
        return res
    }
}

// 创建高阶函数根据参数isReadonly,来创建不同类型的get函数
function createGetter(isReadonly = false) {
    return function get(target, key) {
        const res = Reflect.get(target, key)
        if (!isReadonly) {
            track(target, key)
        }
        return res
    }

}
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)

export const mutableHanlders = {
    get,
    set
}


export const readonlyHanlders = {
    get: readonlyGet,
    set(target, key, value) {
        console.warn(`key:${key}是只读的,不可修改`)
        return true
    }
}

随着Proxy handler逻辑的抽离,reactive.ts的逻辑更加单一

+ import { mutableHanlders, readonlyHanlders } from './baseHandlers'
- import { track, trigger } from './effect'


- function createSetter() {
-     return function set(target, key, value) {
-         let res = Reflect.set(target, key, value)
-         trigger(target, key)
-         return res
-     }
- }
- // 创建高阶函数根据参数isReadonly,来创建不同类型的get函数
- function createGetter(isReadonly = false) {
-     return function get(target, key) {
-         const res = Reflect.get(target, key)
-         if (!isReadonly) {
-             track(target, key)
-         }
-         return res
-     }
- 
- }
- const get = createGetter()
- const set = createSetter()
- const readonlyGet = createGetter()

export const reactive = (raw: any) => {
-    return new Proxy(raw, {
-        get,
-        set
-    })
+    return new Proxy(raw, mutableHanlders)
}
export function readonly(raw: any) {
-   return new Proxy(raw, {
-       get: readonlyGet,
-       set(target, key, value) {
-           console.warn(`key:${key}是只读的,不可修改`)
-           return true
-       }
-   })
+   return new Proxy(raw, readonlyHanlders)
}

这样就完成了这部分代码的优化

接下来,来思考如何实现isReactive、isReadonly

既然我们已经使用Proxy拦截到了reactive对象的get set操作,那么我们是否可以在isReactive方法调用时,返回一个自定义的key,然后在get方法中,判断如果key是我们自定义的那一个,那么我们就直接返回true呢?

答案是可以的,因为源码中也是这样的实现的 🤣

修改baseHandlers.ts


// 创建高阶函数根据参数isReadonly,来创建不同类型的get函数
function createGetter(isReadonly = false) {
    return function get(target, key) {
        const res = Reflect.get(target, key)
+       if (key === ReactiveFlags.IS_REACTIVE) {
+           return !isReadonly
+       }
        if (!isReadonly) {
            track(target, key)
        }
        return res
    }
}

修改reactive.ts

+ export const enum ReactiveFlags {
+     IS_REACTIVE = "__v_isReactive"
+ }

isReadonly同样的方法来实现

修改reactive.ts

+ export const enum ReactiveFlags {
-    IS_REACTIVE = "__v_isReactive"
+    IS_REACTIVE = "__v_isReactive",
+    IS_READONLY = "__v_isReadonly"
+ }

修改baseHandlers.ts

// 创建高阶函数根据参数isReadonly,来创建不同类型的get函数
function createGetter(isReadonly = false) {
    return function get(target, key) {
        const res = Reflect.get(target, key)
+       if (key === ReactiveFlags.IS_REACTIVE) {
+           return !isReadonly
-       }
+       }else if (key === ReactiveFlags.IS_READONLY) {
+           return !isReadonly
+       }
        if (!isReadonly) {
            track(target, key)
        }
        return res
    }
}

然后就可以增加isReactive、isReadonly和isProxy方法了

继续修改reactive.ts

+ export function isReactive(value) {
+     // 如果不是reactive对象,为了返回布尔值,使用!!将结果转换成刚bool类型
+     return !!value[ReactiveFlags.IS_REACTIVE]
+ }
+ 
+ export function isReadonly(value) {
+     return !!value[ReactiveFlags.IS_READONLY]
+ }
+ export function isProxy(value) {
+     return isReactive(value) || isReadonly(value)
+ }

好了,最后我们执行yarn test readonly,可以发现两个用例都已经通过,说明目前逻辑没有问题

image.png

2.嵌套对象的reactive

老规矩,先看测试。 修改reactive.spec.ts如下

+ it("nested reactive",() => {
+     const original = {
+         nested:{
+             foo: 1
+         },
+         array: [{ bar: 2}]
+     }
+     const observed = reactive(original)
+     expect(isReactive(observed)).toBe(true)
+     expect(isReactive(observed.nested)).toBe(true)
+     expect(isReactive(observed.array)).toBe(true)
+     expect(isReactive(observed.array[0])).toBe(true)
+ })

先执行yarn test reactive进行测试,结果没有通过

接下来来分析一下原因,可以发现,与上面不同的是,当前reactive要处理的对象是一个嵌套的多层结构,但是我们之前的逻辑是没有对多层嵌套对象进行深度处理的。

解决办法就是基于之前完成的reactive功能,在getter中如果遇到value是对象类型,进行递归的处理即可

修改baseHandlers.ts

- import { ReactiveFlags } from './reactive'
+ import { reactive, ReactiveFlags } from './reactive'

function createGetter(isReadonly = false) {
    return function get(target, key) {
        const res = Reflect.get(target, key)
+       if(isObject(res)) {
+           return reactive(res)
+       }
        if (key === ReactiveFlags.IS_REACTIVE) {
            return !isReadonly
        }else if (key === ReactiveFlags.IS_READONLY) {
            return !isReadonly
        }
        if (!isReadonly) {
            track(target, key)
        }
        return res
    }

}

这里我们新增文件夹src/shared/在文件夹下新增index.ts,用来存放常用的工具函数

export function isObject (value) {
    return typeof value === 'object' && value !== null
}

同样对应的readonly也做相同处理

- import { reactive, ReactiveFlags } from './reactive'
+ import { reactive, ReactiveFlags, readonly } from './reactive'

function createGetter(isReadonly = false) {
    return function get(target, key) {
        const res = Reflect.get(target, key)
+       if(isObject(res)) {
-           return reactive(res)
+           return isReadonly ? readonly(res) : reactive(res)
+       }
        ...
        return res
    }

}

重新执行测试,可以发现测试通过了

3.shallowReadonly功能

和上面一样,先增加测试来初步认识一下这个功能

增加shallowReadonly.ts文件

import { isReadonly, shallowReadonly } from '../reactive'

describe('shallowReadonly', () => {
    it("happy path", () => {
        const props = shallowReadonly({ n: { foo: 1 } })
        expect(isReadonly(props)).toBe(true)
        expect(isReadonly(props.n)).toBe(false)

    })

    it('warn then call set',() => {
        console.warn = jest.fn()
        const user = shallowReadonly({
            age:10
        })
        user.age = 11
        expect(console.warn).toBeCalledTimes(1)
    })
})

可以看到,经过shallowReadonly处理的对象,只有最外层的属性是只读的,访问内层属性依然可以

了解这个功能之后,我们就可以着手写代码了,类型isReadonly,我们可以在createGetter中增加另外一个参数isShallow表示当前的getter是不是浅层的处理

修改baseHandlers.ts如下

// 创建高阶函数根据参数isReadonly,来创建不同类型的get函数
function createGetter(isReadonly = false, isShallow = false) {
    return function get(target, key) {
        const res = Reflect.get(target, key)
+       if (key === ReactiveFlags.IS_REACTIVE) {
+           return !isReadonly
+       } else if (key === ReactiveFlags.IS_READONLY) {
+           return !isReadonly
+       }
        if (isShallow) {
            return res
        }
        if (isObject(res)) {
            return isReadonly ? readonly : reactive(res)
        }
-       if (key === ReactiveFlags.IS_REACTIVE) {
-           return !isReadonly
-       } else if (key === ReactiveFlags.IS_READONLY) {
-           return !isReadonly
-       }
        if (!isReadonly) {
            track(target, key)
        }
        return res
    }

}
+ const shallowReadonlyGet = createGetter(true, true)



+ export const shallowReadonlyHanlders = {
+     get: shallowReadonlyGet,
+     set(target, key, value) {
+         console.warn(`key:${key}是只读的,不可修改`)
+         return true
+     }
+ }

这里我们又可以发现shallowReadonlyHanlders和readonlyHanlders存在重复的地方,所以可以在shared/index.ts中增加一个extend方法用于重写对象上的属性

export function isObject(value) {
    return typeof value === 'object' && value !== null
}

+ export function extend() {
+     return Object.assign
+ }

然后,修改baseHandlers.ts中的shallowReadonlyHanlders

- export const shallowReadonlyHanlders = {
-     get: shallowReadonlyGet,
-     set(target, key, value) {
-         console.warn(`key:${key}是只读的,不可修改`)
-         return true
-     }
- }
+ export const shallowReadonlyHanlders = extend({},readonlyHanlders,{
+     get: shallowReadonlyGetter
+ })

最后,再执行yarn test,可以看到全部的11个用例都已经通过了

image.png

至此,这部分功能实现完成,敬请期待下篇文章的更新。🙌