🚀实现 shallowReadonly 和 ref 功能 且利用 Jest 测试
山不在高,有仙则名。水不在深,有龙则灵。 —— 刘禹锡《陋室铭》
前言
上篇对 reactive & effect 的补充暂告一段落,还没看过上一篇的看这里 🎉
整篇文章的通过TDD测试驱动开发,带你一步一步实现 vue3 源码。
本篇文章内容包括:
- 讲解 shallowReadonly 和 shallowReactive 的实现
- 讲解 isShallow 的实现思路
- 讲解 isProxy 的实现思路
- 讲解 toRaw 的实现思路
- 讲解 ref 的实现思路
🤣 如有错漏,请多指教 ❤
实现 shallowReadonly
首先要搞清楚 shallowReadonly 是什么东西,以及它的用法,咱们才好往下实现它。
创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(暴露原始值)
我们逐句讲解:
-
创建一个 proxy
跟 readonly 一样,我们也需要 new 一个 Proxy,并通过传入 handler。
-
使其自身的 property 为只读
跟 readonly 一样,我们需要传入 handler 的 getter 和 setter,用户对数据进行 set 赋值操作的时候会得到一个警告。
-
不执行嵌套对象的深度只读转换
与 readonly 不一样的地方在于,我们不用通过
isObject()判断 get 操作之后得到的res是否是对象然后再次执行 readonly(res)。只需要判断数据是否 isShallow,如果是,直接return res即可。
下面是我们的测试用例:
it('shallowReadonly basic test', () => {
let original = {
foo: {
name: 'ghx',
},
}
let obj = shallowReadonly(original)
expect(isReadonly(obj)).toBe(true)
// 因为只做表层的readonly,深层的数据还不是proxy
expect(isReadonly(obj.foo)).toBe(false)
expect(isReactive(obj.foo)).toBe(false)
})
- 创建一个 proxy需要传入不一样的 handlers
export function shallowReadonly<T>(target: T) {
return createReactiveObject(target, shallowReadonlyHandlers)
}
- 使其自身的 property 为只读
这里我通过 extend 拓展了一下readonlyhandlers,但是 get 操作需要单独处理。
export const shallowReadonlyHandlers: ProxyHandler<object> = extend({}, readonlyHandlers, {
get: shallowReadonlyGet,
})
-
不执行嵌套对象的深度只读转换
判断数据是否 isShallow,改写
createGetter,加多一个入参isShallow
export function createGetter<T extends object>(isReadonly = false, isShallow = 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)
if (!isReadonly) {
track(target, key as string)
}
// 这句是当前功能的关键
if (isShallow) {
return res
}
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
为什么不把 isShallow 判断放在 isReadonly 之前?
这就涉及到后面的 shallowReactive 实现了,shallowReactive 除了要返回 get 到的值之外,同时还要进行依赖收集。
之后创建一个常量用于缓存 createGetter 返回的函数即可。
const shallowReadonlyGet = createGetter(true, true)
实现 shallowReactive
创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (暴露原始值)。
// shallowReactive的get操作
const shallowGet = createGetter(false, true)
// shallowReactive的set操作
const shallowSet = createSetter(true)
export const shallowReactiveHandlers: ProxyHandler<object> = extend({}, mutableHandlers, {
get: shallowGet,
set: shallowSet,
})
export function shallowReactive<T extends object>(target: T) {
return createReactiveObject<T>(target, shallowReactiveHandlers)
}
上面有讲isShallow,那我们也实现一个判断是否开启shallow模式的函数吧。
实现isShallow
export enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly',
IS_SHALLOW = '__v_isShallow',
RAW = '__v_raw'
}
// 检查对象是否 开启 shallow mode
export function isShallow(value: unknown){
return !!(value as Target)[ReactiveFlags.IS_SHALLOW]
}
上面就是故意触发proxy的get操作,因为createGetter()的入参有个isShallow,这已经为我们标识当前proxy是否是shallow了。
我们改写以下createGetter,加多一层判断,如果get操作的key值是ReactiveFlags.IS_SHALLOW,那么直接返回入参isShallow的状态。
export function createGetter<T extends object>(isReadonly = false, isShallow = 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
} else if (key === ReactiveFlags.IS_SHALLOW) {
// 主要代码:
return isShallow
} else if (key === ReactiveFlags.RAW) {
return target
}
let res = Reflect.get(target, key)
if (!isReadonly) {
track(target, key as string)
}
if (isShallow) {
return res
}
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
实现 isProxy
检查对象是否是由 reactive 或 readonly 创建的 proxy。
判断 reactive 和 readonly 生成的 proxy,在上一章节已经讲了,所以这里实现起来比较容易。
export function isProxy(value: unknown) {
return isReactive(value) || isReadonly(value)
}
实现 toRaw 功能
返回 reactive 或 readonly 代理的原始对象。这是一个“逃生舱”,可用于临时读取数据而无需承担代理访问/跟踪的开销,也可用于写入数据而避免触发更改
总而言之就是可以通过toRaw获取到reactive或者readonly的原始值。
以下是测试用例
it('toRaw', () => {
const original = { foo: 1 }
const observed = reactive(original)
// 输出的结果必须要等于原始值
expect(toRaw(observed)).toBe(original)
expect(toRaw(original)).toBe(original)
})
it('nested reactive toRaw', () => {
const original = {
foo: {
name: 'ghx',
},
}
const observed = reactive(original)
const raw = toRaw(observed)
expect(raw).toBe(original)
expect(raw.foo).toBe(original.foo)
})
怎么获取原始数据呢?
可以在 get 操作直接 return get()的第一个参数target。
那我们怎么才能知道什么时候应该 return target呢?我们需要一个标识,这个标识也是之前讲过,判断 isReadonly 和 isReactive 也是用的这个标识。
在 ReactiveFlags 定义一个枚举成员RAW,用于标记当前 get 操作是因为 raw 引起的
export enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly',
IS_SHALLOW = '__v_isShallow',
RAW = '__v_raw',
}
之后我们就可以通过故意触发proxy的 get 操作,让 get 操作 return 原始值了。
// 返回 reactive 或 readonly 代理的原始对象
export function toRaw<T>(observed: T): T {
// observed存在,触发get操作,在createGetter直接return target
const raw = observed && (observed as Target)[ReactiveFlags.RAW]
return raw ? toRaw(raw) : observed
}
实现 ref
- 接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。
- 如果将对象分配为 ref 值,则它将被 reactive 函数处理为深层的响应式对象。
内部值一般指值类型(string,number,boolean。。。)
为什么要有 ref 呢,reactive 不行吗?
因为reactive用的是 proxy,而proxy只能针对对象去监听数据变化,基本数据类型并不能用 proxy。
所以我们想到了class里面的取值函数 getter 和存值函数 getter,他们都能在数据变化的时候对数据加以操作。
我们依旧如此编写一个测试用例来驱动开发
it('should hold a value', () => {
const a = ref(1)
expect(a.value).toBe(1)
a.value = 2
expect(a.value).toBe(2)
})
定义一个 RefImpl 类,实例化的就是 ref 对象
class RefImpl<T> {
private _value: T
constructor(value: any) {
this._value = value
}
get value() {
return this._value
}
set value(newValue: any) {
this._value = newValue
}
}
很好,上面的测试用例我们已经跑通了
但是光跑通上面的简单用例还不行,我们必须让 ref 具有响应式行为
it('should be reactive', () => {
const a = ref(1)
let dummy
let calls = 0
effect(() => {
calls++
dummy = a.value
})
expect(calls).toBe(1)
expect(dummy).toBe(1)
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
// same value should not trigger
a.value = 2
expect(calls).toBe(2)
})
在实现reactive数据响应式的时候我们就已经实现了依赖收集和触发依赖功能,为了对代码能有更好的可读性和性能优化,我们会选择复用reactive的依赖收集和触发依赖的相关代码逻辑。
但是这些代码逻辑已经写死在 trigger()和 track()函数里面了,为了代码能复用,我们可以把这些逻辑抽离出来。
定义一个trackEffect函数,对依赖收集的逻辑进行封装
export type Dep = Set<ReactiveEffect>
export function trackEffect(dep: Dep) {
// 避免不必要的add操作
if (dep.has(activeEffect)) return
// 将activeEffect实例对象add给deps
dep.add(activeEffect)
// activeEffect的deps 接收 Set<ReactiveEffect>类型的deps
// 供删除依赖的时候使用(停止监听依赖)
activeEffect.deps.push(dep)
}
定义一个triggerEffect函数,对触发依赖的逻辑进行封装
export function triggerEffect(dep: Dep) {
for (let effect of dep) {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
我们就可以在 RefImpl 类的 get 和 set 进行依赖收集和触发依赖,不过在此之前,我们还需要定义一个dep公有成员函数,用于存储这一个ref对象的依赖
class RefImpl<T> {
private _value: T
public dep?: Dep = undefined
constructor(value: any) {
this._value = value
this.dep = new Set()
}
get value() {
trackRefValue(this)
return this._value
}
set value(newValue: any) {
// 触发依赖
// 注意这里先赋值再触发依赖
this._value = newValue
triggerEffect(this.dep as Dep)
}
}
function trackRefValue(ref: RefImpl<any>) {
// 有时候根本就没有调用effect(),也就是说activeEffect是undefined的情况
// isTracking 的实现在之前的章节讲到
if (isTracking()) {
// 依赖收集
trackEffect(ref.dep as Dep)
}
}
当 ref 对象被赋予相同的值的时候,我们要做到不触发依赖的行为。
怎么才能判断是不是相同的值呢?
我们必须给 RefImpl 存入一个原始值_rawValue,它存放着 ref 的入参(ref 原本传入的值),我们只需要把当前执行 set 赋值操作的value和_rawValue进行比较,如果相同,我们就不触发依赖。
比较的方式我们选择使用ES6 的 Object.is
export function hasChanged(value: any, oldValue: any) {
return !Object.is(value, oldValue)
}
比较结果为 true,说明value已经改变了,这时我们应该更新_rawValue的值,并触发依赖。
class RefImpl<T> {
private _value: T
public dep?: Dep = undefined
private _rawValue: T
constructor(value: any) {
this._value = value
this._rawValue = value
this.dep = new Set()
}
get value() {
trackRefValue(this)
return this._value
}
set value(newValue: any) {
// 触发依赖
// 对比旧的值和新的值,如果相等就没必要触发依赖和赋值了,这也是性能优化的点
if (hasChanged(newValue, this._rawValue)) {
// 注意这里先赋值再触发依赖
this._value = newValue
this._rawValue = newValue
triggerEffect(this.dep as Dep)
}
}
}
如果将对象分配为 ref 值,则它将被 reactive 函数处理为深层的响应式对象。
reactive 函数的深层响应式对象功能,在之前的篇章里我们已经实现了。
我们只需要判断value是否是对象,是对象就用reactive处理,不是对象就直接给_value
function convert(value: any) {
return isObject(value) ? reactive(value) : value
}
class RefImpl<T> {
private _value: T
public dep?: Dep = undefined
private _rawValue: T
constructor(value: any) {
this._value = convert(value)
this._rawValue = value
this.dep = new Set()
}
get value() {
trackRefValue(this)
return this._value
}
set value(newValue: any) {
// 触发依赖
// 对比旧的值和新的值,如果相等就没必要触发依赖和赋值了,这也是性能优化的点
if (hasChanged(newValue, this._rawValue)) {
// 注意这里先赋值再触发依赖
this._value = convert(newValue)
this._rawValue = newValue
triggerEffect(this.dep as Dep)
}
}
}
至此,测试用例已全部跑通。
总结
shallowReactive和shallowReadonly只能对表层的数据提供reactive和readonly操作,对于深层的数据我们并只能暴露原始值。
isProxy 实现的如此简单是因为我们在之前有刻意的封装函数,希望我们在写代码的时候也要有这种意识,有效的封装函数会是代码写的更简单。
ref 和 reactive 的区别:
- ref 一般接受的是值类型,reactive 接受的是引用类型,虽然 ref 能接受对象类型,但是其内部还是使用了 reactive(),所以某些情况下直接使用 reactive 会更好一些。(为什么?你问 proxy 这个大哥给不给你传入值类型啊!)
- ref 需要通过.value 才能拿到值。
还有其他区别欢迎补充!