这一次让我们一起来实现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测试之后,发现第二个用例已经通过
然后,来看第一个用例,也就是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,可以发现两个用例都已经通过,说明目前逻辑没有问题
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个用例都已经通过了
至此,这部分功能实现完成,敬请期待下篇文章的更新。🙌