实现ref
主要思路
ref主要是在reactive的基础上新增加了对基本数据类的响应式,因此对于传入进来的基本数据类型的数据,可以返回一个被RefImpl类型包裹的实例对象出去。 然后对这个对象进行get 和 set 拦截操作,实现响应式。
代码
先实现一个RefImpl的类
对传入的value 进行 get 收集依赖, set触发依赖
import { hasChanged, isObject } from "../shared"
import { trackEffects, triggerEffects } from "./effect"
import { reactive } from "./reactive"
/**
* ref 接受 1 true “1” 这样的基本数据类的值
* 但是proxy只能代理对象,于是对于传入的值将其改造成一个RefImpl的实例对象
* 通过对这个对象进行get 和 set 操作进行拦截,达到的响应的目的
*/
class RefImpl {
private _value: any
private dep: any
private _rawValue: any
constructor(value) {
this._rawValue = value // 存储开始的值
this._value = convert(value) // 如果不是响应式的对象需要转换成响应式的,如果只是普通数据类型的数据那么不管
this.dep = new Set()
}
get value() {
// 收集依赖
trackEffects(this.dep)
return this._value
}
set value(newValue) {
// 触发依赖
// 这里比较的是数据是 一个是proxy 一个是原始值 ( 受限于 Object.is() ),所以需要转换
if (hasChanged(newValue, this._rawValue)) { // 判断当前值和原来的值是否相等 如果相等则不触发更新
this._rawValue = newValue // 原始值也需要更新,因为下次新旧值对比的时候,拿的是_rawValue进行对比的
this._value = convert(newValue) // 注意需要修改value之后再去通知effect执行
triggerEffects(this.dep)
}
}
}
export function convert(value) {
return isObject(value) ? reactive(value) : value
}
export function ref(value) {
return new RefImpl(value)
}
一些细节
ref 对于 传入的两次相同的值来说,最后一次是不会触发依赖的 所以需要比较新传入的值和当前的值是否相同,如果不相同再去触发依赖, 但由于Object.is 函数只能比较两个普通对象是否相等,所以在一开始我们需要设置一个变量_rawValue 去保存 初始值。
单元测试
import { effect } from '../effect'
import { ref } from '../ref'
describe("ref", () => {
it("happy path", () => {
const a = ref(1)
expect(a.value).toBe(1)
})
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 的 set 函数,
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
// same value should not trigger
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
})
it("should make nested properties reactive", () => {
const a = ref({
count: 1,
})
let dummy
effect(() => {
dummy = a.value.count
})
expect(dummy).toBe(1)
// ref 复杂数据类型 触发 set 操作
a.value.count = 2
expect(dummy).toBe(2)
})
})