ref
第一步
测试用例
describe('ref', () => {
it('happy path', () => {
// 创建一个ref:a
const a = ref(1)
// a.value === 1
expect(a.value).toBe(1)
})
})
功能描述
使用ref创建一个响应式数据(传入原始值),可以通过ref.value来访问它
实现
// 创建ref对象的方法
function ref (value) {
return new RefImpl(value)
}
// 创建一个类,用它的实例来包装一个ref,与reactiveEffect一个道理
class RefImpl {
private _value
constructor(value) {
this._value = value
}
// 定义get和set方法
get value () {
return this._value
}
// todo
set value (newValue) {
}
}
第一步很简单,已经实现了通过value来访问ref的值
第二步
测试用例
describe('ref', () => {
// 第一步的功能点,不用关心
it('happy path', () => {
const a = ref(1)
expect(a.value).toBe(1)
})
// 第二步需要实现的功能点
it('should be reactive', () => {
// 创建一个ref:a
let a = ref(1)
let dummy
// 声明一个变量用来计数
let calls = 0
// 通过effect做依赖收集
effect(() => {
calls++
dummy = a.value
})
// 上一步effect做依赖收集回先执行一次传入的fn,calls === 1,dummy === 1
expect(calls).toBe(1)
expect(dummy).toBe(1)
// 设置value的值,触发set
a.value = 2
// 传入effect的fn又执行了一次,calls计数加1:calls === 2,触发依赖后dummy === 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
})
})
功能描述
第二不需要实现的是收集和触发响应式对象的依赖,如测试用例描述那样
实现
// 更新RefImpl
class RefImpl {
private _value
public dep
private _rawValue
constructor(value) {
this._value = value
// 初始化dep,需要一个dep来保存依赖
this.dep = new Set()
}
get value () {
/*
* reactive代理的是对象,对象有多个属性值,而且属性值也有可能是对象,
* 所以它使用了Map来保存多个属性的依赖
* 因为ref代理的是原始值,它只有一个value属性,所以只需要一个set来存储这个属性的依赖项就可以
*/
/*
* 如果activeEffect = undefined不需要收集依赖
* 这里其实不需要关心 shouldTrack,因为shouldTrack的相关逻辑在实现reactive的时候已经实现了
* 在这里shouldTrack与reactive的用处和实现reactive是的用处是一样的
*/
if (!isTracking()) return
// 如果已经收集了,直接return掉
if (dep.has(activeEffect)) return
dep.add(activeEffect)
activeEffect.deps.push(dep)
return this._value
}
set value (newValue) {
// 一定需要先去修改value,再去触发依赖
this._value = newValue
// 遍历ref的value属性的所有所有依赖项并执行
for (const effect of dep) {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
}
优化
对之前实现的reactive还有印象的话,不难发现,实现ref收集依赖和触发依赖的代码有重复,所以可以将它抽出来
// 执行触发依赖的操作
function triggerEffects (dep) {
for (const effect of dep) {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
// 执行收集依赖的操作
function trackEffects (dep) {
if (dep.has(activeEffect)) return
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
// 更新RefImpl
class RefImpl {
... // 省略
get value () {
// todo 这一步也可以抽离出来
// if (!isTracking()) return
// trackEffects(this.dep)
// 抽离后
trackRefValue(this)
return this._value
}
set value (newValue) {
this._value = newValue
triggerEffects(this.dep)
}
// get收集依赖时的操作也可以进行抽离
function trackRefValue (ref) {
if (isTracking()) {
trackEffects(ref.dep)
}
}
第三步
测试用例
describe('ref', () => {
it('happy path', () => {
... // 省略
})
it('should be reactive', () => {
... // 省略第一二步的测试
/*
* 第三步
* 再次修改a.value的值,和上一次的值相等,那么它就不应该触发依赖
* calls不变:calls === 2
* 没有触发依赖:dummy === 2
* */
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
})
})
功能描述
第三步也很简单,修改ref的值,如果修改的值和当前的值一样,那么不触发依赖
实现
class RefImpl {
private _value
public dep
constructor(value) {
// 如果这个value是一个对象,那么需要通过reactive给它进行包装
// 1.检查value是不是对象
this._value = value
this.dep = new Set()
}
get value () {
trackRefValue(this)
return this._value
}
set value (newValue) {
/*
* newValue -> this._value,如果相等不需要执行操作
* */
if (hasChange(newValue, this._value)) {
// 一定需要先去修改value,再去触发依赖
this._value = newValue
triggerEffects(this.dep)
}
}
}
// 对比新旧值有变化(两个值是否相等)
function hasChange = (newValue, oldValue) => !Object.is(newValue, oldValue)
第四步
测试用例
it('should make nested properties reactive', () => {
// 如果ref接受的值是一个对象
const a = ref({ count: 1 })
let dummy
// 它的值也是通过.value去取的
effect(() => {
dummy = a.value.count
})
// 调用effect会调用传入的fn,dummy === 1
expect(dummy).toBe(1)
// 修改ref的值
a.value.count = 2
// dummy的值也会被修改
expect(dummy).toBe(2)
})
功能描述
ref可以将原始值处理为响应式对象,那么如果传入一个对象呢,与原始值相似,也是通过ref.value可以访问到传入的这个值,但是呢因为它的value是一个对象,所以需要将它用reactive包装起来再返回,它依旧是一个响应式数据。
实现
// 更新RefImpl
class RefImpl {
private _value
public dep
private _rawValue // 声明一个值来保存传入的值(无论是基本数据类型还是引用类型都保存起来)
constructor(value) {
// 如果这个value是一个对象,那么需要通过reactive给它进行包装
// 1.检查value是不是对象
this._value = convert(value)
this._rawValue = value
// this._value = value
this.dep = new Set()
}
get value () {
trackRefValue(this)
return this._value
}
set value (newValue) {
// if (Object.is(newValue, this._value)) return
/*
* 对比的时候呢,是需要对两个原始值进行对比,而如果value是一个对象的话,this._value就成了reactive
* 所以传入的value如果是对象,在对比前需要将reactive转为原始对象
* 解决办法:初始化RefImpl的时候,声明一个值(_rawValue)保存传入的value,对比的时候就使用_rawValue对比
* */
if (hasChange(newValue, this._rawValue)) {
// 一定需要先去修改value,再去触发依赖
this._rawValue = newValue
this._value = convert(newValue)
triggerEffects(this.dep)
}
}
}
/*
* 给_value赋值时,需要先判断它时引用类型还是基本数据类型,如果是引用类型,需要用reactive包装
* isObject前面已经实现过了
*/
function convert (value) {
return isObject(value) ? reactive(value) : value
}