12_实现ref功能
12_实现ref功能
一、单元测试
首先建立ref.spec.ts
,然后来看一下ref
的happy path
。
// src/reactivity/tests/ref.spec.ts
describe('ref', function () {
it('happy path', () => {
const a = ref(1);
expect(a.value).toBe(1);
a.value = 2;
expect(a.value).toBe(2);
});
})
分析单测,我们可以看出,主要就是两个关注点:
- 通过
ref
声明的响应式变量可以通过.value
的形式来读取,也就是get
操作 - 同时也可以设置值,也就是
set
操作
二、代码实现 happy path
建立ref.ts
。
// src/reactivity/ref.ts
// + 这里同样封装一个类来实现各种操作
class RefImpl {
private _value: any;
constructor(value: any) {
this._value = value;
}
get value() {
return this._value;
}
set value(newVal: any) {
this._value = newVal;
}
}
export function ref(value) {
return new RefImpl(value);
}
我们在class
顶层定义一个_value
来存储传进来的value
,然后后续操作这个_value
就够了,可以看到happy path
的实现是比较简单的。

三、完善逻辑 v2.0
首先来看第一个单测的逻辑。
// src/reactivity/tests/ref.spec.ts
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);
// + 设置同样的value不应该再次触发更新
// a.value = 2;
// expect(calls).toBe(2);
});
先运行一下单测,看看哪里会出问题,然后针对性的去结局问题。

可以看到第二个用例测试失败,我们期望calls
为2
,实际值为1
,说明effect
没有调用第二次,也就是当a
的值发生变化后,依赖没有被重新触发。这很容易理解,因为我们根本就没去收集依赖。
那接下来,我们就来完善这部分内容?其实也不难,回去看effect
就能知道,我们已经做过相关的内容了。只需要抽离封装部分代码,然后复用即可。
// src/reactivity/effect.ts
// + 抽离dep的收集逻辑
export function trackEffects(dep) {
if (dep.has(activeEffect)) return;
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
// + 抽离dep的触发逻辑
export function triggerEffects(dep) {
for (const effect of dep) {
if (effect.scheduler) {
// ps: effect._fn 为了让scheduler能拿到原始依赖
effect.scheduler(effect._fn);
} else {
effect.run();
}
}
}
再回到ref.ts
,将effect
中抽离的trackEffects
和triggerEffects
集成进来。
// src/reactivity/ref.ts
import { triggerEffects, trackEffects } from './effect';
class RefImpl {
private _value: any;
public dep;
constructor(value: any) {
this._value = value;
this.dep = new Set();
}
get value() {
trackEffects(this.dep);
return this._value;
}
set value(newVal: any) {
this._value = newVal;
triggerEffects(this.dep);
}
}
export function ref(value) {
return new RefImpl(value);
}
再跑一遍单测。

白给,用例1出现了错误,activeEffect
没了,是undefined
。
分析一下,大概就是两个问题:
-
为什么
activeEffect
会是undefined
?activeEffect
是在run
的时候去赋值的,也就是必须要有相关的effect
。在第一个测试用例中,可以看出我们并没有相关的依赖,所以也就不存在依赖收集的情况。 -
该怎么解决这个问题? 那这么说,我们此时并不需要去收集依赖。实际上,我们之前的
isTracking
就是用来判断,是否应该收集依赖。加上即可。
if (isTracking()) {
trackEffects(this.dep);
}
return this._value;

可以看到,用例1通过了。
那我们继续放开下面两行,继续完善用例2最后的逻辑。那还是老样子,先跑一下单测,确定一下问题所在。

依旧很轻松可以看出,设置同样的值,calls
加一,意思是effect
还是被触发了一遍,这跟我们预想的并不一致。
那我们就需要在triggerEffects
前判断,新设置的值是否有变化。
if (Object.is(newVal, this._value)) {
this._value = newVal;
triggerEffects(this.dep);
}
可能后续会经常用到这类函数,所以我们考虑封装一下进shared
里面。
// src/shared/index.ts
export const hasChanged = (val, newVal) => {
return !Object.is(val, newVal);
};
那继续回去用起来。
if (hasChanged(newVal, this._value)) {
this._value = newVal;
triggerEffects(this.dep);
}
逻辑差不多完善,跑一下单测。

通过,美滋滋。
四、完善逻辑 v3.0
// src/reactivity/tests/ref.spec.ts
it('should make nested properties reactive', () => {
// + 可以接收一个对象,并且也具备响应式
const a = ref({
count: 1
});
let dummy;
effect(() => {
dummy = a.value.count;
});
expect(dummy).toBe(1);
a.value.count = 2;
expect(dummy).toBe(2);
});
其实这里的处理就很简单了,当我们用_value
去存储value
的时候,只需要先判断一下是否是对象,如果是对象,就用reactive
包裹即可。
class RefImpl {
private _value: any;
public dep;
constructor(value: any) {
// + 复用isObject
this._value = isObject(value) ? reactive(value) : value;
this.dep = new Set();
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newVal: any) {
if (hasChanged(newVal, this._value)) {
this._value = newVal;
triggerEffects(this.dep);
}
}
}

至此,三个单元测试全部通过,ref
的基本实现也已经完成,喝彩!!!
ps
这是一个 早起俱乐部!
⭐️ 适合人群:所有想有所改变的人,可以先从早起半小时开始!抽出30分钟,从初心开始!! ⭐️ 没有任何其它意味,只是本人想寻找一起早起、志同道合的小伙伴。
转载自:https://juejin.cn/post/7181710097863671864