🚀 实现Vue3的isReactive & isReadonly 且利用Jest测试
山不在高,有仙则名。水不在深,有龙则灵。 —— 刘禹锡《陋室铭》
前言
上篇对reactive & effect的补充暂告一段落,还没看过上一篇的看这里🎉
整篇文章的通过TDD测试驱动开发,带你一步一步实现vue3源码,文章的最后还有完整代码哦。
本篇文章内容包括:
- 讲解Readonly的实现以及对已有代码的重构
- 讲解isReactive的实现思路
- 讲解isReadonly的实现思路
- 源数据对象嵌套结构的代理对象
实现 Readonly
上篇文章我们已经实现了reactive,其实readonly是reactive的一种特殊情况,只不过是只读的。它也是返回一个proxy对象,并没有set操作,所以readonly并没有依赖触发,既然没有依赖触发,那么它也不需要get操作的依赖收集。
先看看我们的readonly测试用例:
describe('readonly', () => {
it('readonly not set', () => {
let original = {
foo: {
fuck: {
name: "i don't care",
},
},
arr: [{color: '#fff'}]
}
let warper = readonly(original)
expect(warper).not.toBe(original)
expect(warper.foo.fuck.name).toBe("i don't care")
})
it('warning when it be call set operation', () => {
let original = {
username: 'ghx',
}
let readonlyObj = readonly(original)
const warn = jest.spyOn(console, 'warn')
// 给readonly做set操作,将会得到一个warning
readonlyObj.username = 'danaizi'
expect(warn).toHaveBeenCalled()
})
})
如果硬要给reactive的代理对象赋值,那么它将得到一个警告(warn)
export function readonly<T>(target: T) {
return new Proxy(target, {
get(target, key) {
let res = Reflect.get(target, key)
// 无需依赖收集,删除了track()函数
return res
},
set(target, key, value) {
// 无需触发依赖
console.warn(`${target} do not set ${String(key)} value ${value}, because it is readonly`)
return true
},
})
}
对已有代码的重构
之前我们已经实现了 reactive 和 readonly,这时候我们应该反观一下代码,观察代码有没有重复的代码段是需要我们去优化的。 目前的不足:reactive 和 readonly 都有相似的实现,相同的代码段较多,可以抽离出来
简述:
reactive和readonly的传入相同的入参targetreactive和readonly都返回proxy对象reactive和readonly的proxy对象都有get和set方法,但是内部的代码实现有点不同
为了统一处理这些相同的代码逻辑,我们不妨新建文件baseHandlers.ts作为Proxy的第二入参handler的定义文件,又因为返回的都是new Proxy()对象,所以我们可以定义一个createReactiveObject函数,用于统一创建proxy对象,增强代码的可读性。
// In baseHandlers.ts
export function createReactiveObject<T extends object>(target: T, handlers: ProxyHandler<T>) {
return new Proxy(target, handlers)
}
// In reactive.ts
export function reactive<T extends object>(target: T) {
return createReactiveObject<T>(target, mutableHandlers)
}
// 其实就是一个没有set操作的reactive
export function readonly<T extends object>(target: T) {
return createReactiveObject<T>(target, readonlyHandlers)
}
handlers通过作为对象统一传入createReactiveObject,这样就可以统一处理reactive和readonly的不同逻辑。
export const mutableHandlers: ProxyHandler<object> = {
get: function(target: T, key: string | symbol) {
let res = Reflect.get(target, key)
// 依赖收集
track(target, key as string)
return res
},
set: function(target: T, key: string | symbol, value: any) {
let success: boolean
success = Reflect.set(target, key, value)
// 触发依赖
trigger(target, key as string)
return success
},
}
export const readonlyHandlers: ProxyHandler<object> = {
get: function(target: T, key: string | symbol) {
let res = Reflect.get(target, key)
return res
},
set(target, key, value) {
console.warn(`${target} do not set ${String(key)} value ${value}, because it is readonly`)
return true
},
}
仔细观察上面的代码,我们都有相同的set和get,内部同样实现了取值操作,但有所不同的是readonly的set操作会抛出warn,这一点我们可以不需要处理。为了区分reactive和readonlyset和get的不同逻辑,我们需要一个标识isReadonly
抽离相同的set和get代码,我们外部需要定义set和get函数,但是我们需要传入一个标识isReadonly区分该函数到底是reactive的代码逻辑还是readonly的代码逻辑,同时不能为set和get新增其他的入参以防破坏代码的可读性。
我们可以定义一个高阶函数,该函数返回set和set函数,入参是isReadonly。
// In baseHandlers.ts
// 高阶函数,isReadonly默认为false
export function createGetter<T extends object>(isReadonly = false) {
return function get(target: T, key: string | symbol) {
let res = Reflect.get(target, key)
if (!isReadonly) {
// 判断是否readonly
// 依赖收集
track(target, key as string)
}
return res
}
}
export function createSetter<T extends object>() {
return function set(target: T, key: string | symbol, value: any) {
let success: boolean
success = Reflect.set(target, key, value)
// 触发依赖
trigger(target, key as string)
return success
}
}
之后定义不同的handlers对象,用于作为入参传入createReactiveObject
// reactive的handlers
export const mutableHandlers: ProxyHandler<object> = {
get: createGetter(),
set: createSetter(),
}
// readonly的handlers
export const readonlyHandlers: ProxyHandler<object> = {
get: createGetter(true),
set(target, key, value) {
console.warn(`${target} do not set ${String(key)} value ${value}, because it is readonly`)
return true
},
}
此处来隐藏了一个优化点
实现 isReadonly
🧠大家思考一下我们以当前的代码来看,用什么来判断一个代理对象是否readonly?
答案就是createGetter的入参 isReadonly,观察之前对readonly的实现可以知道,让源数据通过Proxy包装之后,就已经在handler的get操作中得知该代理对象是否为readonly代理对象了。
既然在get操作中才能得到isReadonly,我们不妨触发一下get操作吧。
触发get操作有一个前提,那就是通过访问代理对象的属性就能触发get操作。你可能会说我们随便访问这个代理的对象的已知属性就可以触发get操作,然后return是否为readonly的结果不就行了吗?但是如果用户真的只想访问这个代理对象的属性并不想知道你到底是readonly还是reactive,这不就出现bug了吗
为此我们需要捏造一个一个代理对象不存在的属性,就叫__v_isReadonly
定义一个函数isReadonly,用于判断一个代理对象是否为readonly代理对象,该函数通过触发代理对象的get操作,返回一个布尔值。
// 给value做类型批注,让value有以下几个可选属性,不然该死的value飘红 --isReactive函数和isReadonly函数 说的就是你们
export interface Target {
__v_isReadonly?: boolean;
}
export function isReadonly(value: unknown){
return (value as Target)['__v_isReadonly']
}
另外,有了__v_isReadonly属性,我们就知道用户是想通过get操作判断代理对象是否是readonly,还是想通过get操作访问指定的属性值。
我们要做的就是把isReadonlyreturn出去
export function createGetter<T extends object>(isReadonly = false) {
return function get(target: T, key: string | symbol) {
if(key === '__v_isReadonly'){
return isReadonly
}
let res = Reflect.get(target, key)
if (!isReadonly) {
// 判断是否readonly
// 依赖收集
track(target, key as string)
}
return res
}
}
以下为isReadonly的测试用例
it('readonly not set', () => {
let original = {
foo: {
fuck: {
name: 'what',
},
},
arr: [{color: '#fff'}]
}
let warper = readonly(original)
expect(warper).not.toBe(original)
expect(isReadonly(warper)).toBe(true)
expect(isReadonly(original)).toBe(false) ❌
// 测试嵌套对象的reactive状态
expect(isReadonly(warper.foo.fuck)).toBe(true)
// expect(isReadonly(warper.foo.fuck.name)).toBe(true) // 因为name是一个基本类型所以isObject会是false,暂时对name生成不了readonly,涉及到往后的知识点 isRef
expect(isReadonly(warper.arr)).toBe(true)
expect(isReadonly(warper.arr[0])).toBe(true)
expect(warper.foo.fuck.name).toBe('what')
})
开始执行测试很顺畅,isReadonly传入一个代理对象,返回true没问题,嗯?怎么执行传入源数据的时候会测试不通过呢?
原因是源数据并没有被代理,并不能触发get操作,结果就是isReadonly(original)只能返回 undefined,因为original根本就没有__v_isReadonly属性。
那我们只要让它返回false就好了。通过!!双叹号,将它转成布尔值。undefined 会转成false。
export function isReadonly(value: unknown){
return !!(value as Target)['__v_isReadonly']
}
实现 isReactive
isReactive很简单,因为createGetter的入参是个布尔值isReadonly,所以不是isReadonly,就是isReactive。
实现思路和isReadonly一样,只是把isReadonly换成isReactive,然后通过get操作,返回一个布尔值。
export interface Target {
__v_isReadonly?: boolean;
__v_isReactive?: boolean;
}
export function isReactive(value: unknown) {
return !!(value as Target)['__v_isReactive']
}
export function createGetter<T extends object>(isReadonly = false) {
return function get(target: T, key: string | symbol) {
// isReactive和isReadonly 都是根据传入的参数 `isReadonly`来决定是否返回true | false的
if (key === '__v_isReactive') {
return !isReadonly
} else if (key === '__v_isReadonly') {
return isReadonly
}
let res = Reflect.get(target, key)
// 之前都是只实现表面一层的reactive,我们现在实现嵌套对象的reactive
if(isObject(res)){
return isReadonly ? readonly(res) : reactive(res)
}
if (!isReadonly) {
// 判断是否readonly
// 依赖收集
track(target, key as string)
}
return res
}
}
看着一个个字符串的状态,是不是觉得很不爽。我们用typescript的enum管理状态,增强代码可读性。
export enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly'
}
export interface Target {
[ReactiveFlags.IS_REACTIVE]?: boolean;
[ReactiveFlags.IS_READONLY]?: boolean;
}
之后只要将enum的key替换上面裸露的字符串就可以了,这里不都说。
遇到嵌套的对象
遇到嵌套的对象作为源数据生成代理对象时,代理对象的子对象作为参数调用isReactive或者调用isReadonly,会返回false,因为里面的对象并没有被代理。
以下是该情况的测试用例
it('nested reactive',()=>{
let original = {
foo: {
name: 'ghx'
},
arr: [{age: 23}]
}
const nested = reactive(original)
expect(isReactive(nested.foo)).toBe(true) ❌
expect(isReactive(nested.arr)).toBe(true) ❌
expect(isReactive(nested.arr[0])).toBe(true) ❌
expect(isReactive(nested.foo)).toBe(true) ❌
// expect(isReactive(nested.foo.name)).toBe(true) ❌ // 涉及到往后的知识点 isRef
})
要想测试用例通过,我们就必须把嵌套的对象也转成reactive代理对象。
当触发get操作的得到的res,我们追加一个判断,如果发现 res 不是reactive或者readonly,并且res是对象,那么递归调用reactive()或者readonly()。
判断是否是对象我们定义一个isObject在shared/index.ts中。
// 判断value是否object或者array
export const isObject = (value: unknown) => {
return value !== null && typeof value === 'object'
}
因为要在get操作时判断得到的res,我们在createGetter()上面做文章
export function createGetter<T extends object>(isReadonly = false) {
return function get(target: T, key: string | symbol) {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
}
let res = Reflect.get(target, key)
// 之前都是只实现表面一层的reactive,我们现在实现嵌套对象的reactive
if(isObject(res)){
return isReadonly ? readonly(res) : reactive(res)
}
if (!isReadonly) {
// 判断是否readonly
// 依赖收集
track(target, key as string)
}
return res
}
}
优化点
反观mutableHandlers和readonlyHandlers
// reactive的handlers
export const mutableHandlers: ProxyHandler<object> = {
get: createGetter(),
set: createSetter(),
}
// readonly的handlers
export const readonlyHandlers: ProxyHandler<object> = {
get: createGetter(true),
set(target, key, value) {
console.warn(`${target} do not set ${String(key)} value ${value}, because it is readonly`)
return true
},
}
代理对象每次触发proxy的get操作的时候都会调用createGetter(),set操作也是一样的。为了优化代码,减少对createGetter()的调用次数,我们单独抽离createGetter()和createSetter(),用一个常量接收。
// 此处调用一次createSetter和createGetter,为了不在每次使用mutableHandlers的时候重复调用
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)
所以mutableHandlers和readonlyHandlers应该被改写成
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
}
export const readonlyHandlers: ProxyHandler<object> = {
get: readonlyGet,
set(target, key, value) {
console.warn(`${target} do not set ${String(key)} value ${value}, because it is readonly`)
return true
},
}
如果还不明白为什么要抽离出来的同学看这里
总结
- readonly的实现和reactive的实现有点相似,但是有点不同。没有set操作,没有依赖收集,触发依赖。
- isReactive的实现和isReadonly的实现原理一样,都是通过
createGetter()的入参isReadonly判断的。 - 遇到嵌套对象的源数据要生成代理对象,代理对象的子对象也要被代理。我们通过判断是否是对象然后递归调用
reactive()或者readonly()来实现。
最后@感谢阅读
不念过去,不畏将来。
完整代码
// In share/index.ts
// 判断value是否object或者array
export const isObject = (value: unknown) => {
return value !== null && typeof value === 'object'
}
// In reactive.ts
import { createReactiveObject, mutableHandlers, readonlyHandlers } from './baseHandlers'
export enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly'
}
// 给value做类型批注,让value有以下几个可选属性,不然该死的value飘红 --isReactive函数和isReadonly函数 说的就是你们
export interface Target {
[ReactiveFlags.IS_REACTIVE]?: boolean;
[ReactiveFlags.IS_READONLY]?: boolean;
}
export function reactive<T extends object>(target: T) {
return createReactiveObject<T>(target, mutableHandlers)
}
// 其实就是一个没有set操作的reactive
export function readonly<T extends object>(target: T) {
return createReactiveObject<T>(target, readonlyHandlers)
}
export function isReactive(value: unknown) {
// target没有__v_isReactive这个属性,为什么还要写target['__v_isReactive']呢?因为这样就会触发proxy的get操作,
// 通过判断createGetter传入的参数isReadonly是否为true,否则isReactive为true
// 优化点:用enum管理状态,增强代码可读性
return !!(value as Target)[ReactiveFlags.IS_REACTIVE]
}
export function isReadonly(value: unknown){
// 同上
return !!(value as Target)[ReactiveFlags.IS_READONLY]
}
// In baseHandlers.ts
import { track, trigger } from './effect'
import { reactive, ReactiveFlags, readonly } from './reactive'
import { isObject } from '../shared'
// 此处调用一次createSetter和getter,为了不在每次使用mutableHandlers的时候重复调用
const get = createGetter()
const set = createSetter()
const readonlyGet = createGetter(true)
// 高阶函数,
export function createGetter<T extends object>(isReadonly = false) {
return function get(target: T, key: string | symbol) {
// isReactive和isReadonly 都是根据传入的参数 `isReadonly`来决定是否返回true | false的
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
}
let res = Reflect.get(target, key)
// 之前都是只实现表面一层的reactive,我们现在实现嵌套对象的reactive
if(isObject(res)){
return isReadonly ? readonly(res) : reactive(res)
}
if (!isReadonly) {
// 判断是否readonly
// 依赖收集
track(target, key as string)
}
return res
}
}
export function createSetter<T extends object>() {
return function set(target: T, key: string | symbol, value: any) {
let success: boolean
success = Reflect.set(target, key, value)
// 触发依赖
trigger(target, key as string)
return success
}
}
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
}
export const readonlyHandlers: ProxyHandler<object> = {
get: readonlyGet,
set(target, key, value) {
console.warn(`${target} do not set ${String(key)} value ${value}, because it is readonly`)
return true
},
}
export function createReactiveObject<T extends object>(target: T, handlers: ProxyHandler<T>) {
return new Proxy(target, handlers)
}
// In readonly.spec.ts
import { readonly, isReadonly } from '../reactive'
describe('readonly', () => {
it('readonly not set', () => {
let original = {
foo: {
fuck: {
name: 'what',
},
},
arr: [{color: '#fff'}]
}
let warper = readonly(original)
expect(warper).not.toBe(original)
expect(isReadonly(warper)).toBe(true)
expect(isReadonly(original)).toBe(false)
// 测试嵌套对象的reactive状态
expect(isReadonly(warper.foo.fuck)).toBe(true)
// expect(isReadonly(warper.foo.fuck.name)).toBe(true) // 因为name是一个基本类型所以isObject会是false,暂时对name生成不了readonly,涉及到往后的知识点 isRef
expect(isReadonly(warper.arr)).toBe(true)
expect(isReadonly(warper.arr[0])).toBe(true)
expect(warper.foo.fuck.name).toBe('what')
})
it('warning when it be call set operation', () => {
let original = {
username: 'ghx',
}
let readonlyObj = readonly(original)
const warn = jest.spyOn(console, 'warn')
readonlyObj.username = 'danaizi'
expect(warn).toHaveBeenCalled()
})
})
// In reactice.spec.ts
import { reactive, isReactive } from '../reactive'
describe('reactive', () => {
it('reactive test', () => {
let original = { num: 1 }
let count = reactive(original)
expect(original).not.toBe(count)
expect(count.num).toEqual(1)
expect(isReactive(original)).toBe(false)
expect(isReactive(count)).toBe(true)
})
it('nested reactive',()=>{
let original = {
foo: {
name: 'ghx'
},
arr: [{age: 23}]
}
const nested = reactive(original)
expect(isReactive(nested.foo)).toBe(true)
expect(isReactive(nested.arr)).toBe(true)
expect(isReactive(nested.arr[0])).toBe(true)
expect(isReactive(nested.foo)).toBe(true)
// expect(isReactive(nested.foo.name)).toBe(true) // 涉及到往后的知识点 isRef
})
})