本文是 Vue3 源码实战专栏的第 7 篇,从 0-1 实现 ref
功能函数。
官方文档 中对ref
的定义,
接受一个内部值,返回一个响应式的、可更改的 ref
对象,此对象只有一个指向其内部值的属性 .value
。
老规矩还是从单测入手,那ref
函数的实现需要 3 个测试用例:
ref
包裹的对象需要 .value 访问ref
包裹的对象是个响应式对象ref
不仅仅可以应用在单值上,对象类型也是响应式的新建ref.spec.ts
,添加第一个测试用例 happy path
it("happy path", () => {
const original = ref(1);
expect(original.value).toBe(1);
});
新建文件 ref.ts
ref 函数接受的是一个基本类型的单值,需要将其转换成对象可以通过value
来访问,可以使用class类,get
语法将对象属性绑定到查询该属性时将被调用的函数。
class RefImpl {
private _value: any;
constructor(value) {
this._value = value;
}
get value() {
return this._value;
}
}
export function ref(value) {
return new RefImpl(value);
}
执行单测yarn test ref
it("should be reactive", () => {
let data = ref(1);
let dummy;
let calls = 0;
effect(() => {
calls++;
dummy = data.value;
});
expect(calls).toBe(1);
expect(dummy).toBe(1);
data.value = 2;
expect(calls).toBe(2);
expect(dummy).toBe(2);
data.value = 2;
expect(calls).toBe(2);
expect(dummy).toBe(2);
});
依据effect
进行依赖收集和触发依赖,calls
表示effect
函数调用次数,calls
值变化说明effect
函数被调用了;
首先effect
作用函数执行,当该函数调用了,断言dummy
变量值就是赋值的data
的值;当更新data
的值后,effect
作用函数被调用,此时的dummy
也要响应式的同步更新;在data
重复赋值相同值时,effect
作用函数不会执行,也就意味着不会进行依赖收集和触发依赖。
ref
的依赖收集和触发依赖,逻辑上应该和reactive
一样,那相应的实现都是effect
中,但是它们的区别就是,ref
可以接受的是单值,就不能套用原本的依赖收集track
函数中按照key
来映射dep
这样的方式。
因为是单值,所以可以定义一个Set结构dep,直接将单值存放在dep中,相当于与之前实现reactive
时track
方法中照key
来映射dep
的逻辑移除了就可以了。
那为了代码的复用性,需要对之前effect
中track
和trigger
进行重构。
export function track(target, key) {
if (!isTracking()) return;
let depMap = targetMap.get(target);
if (!depMap) {
depMap = new Map();
targetMap.set(target, depMap);
}
let dep = depMap.get(key);
if (!dep) {
dep = new Set();
depMap.set(key, dep);
}
trackEffects(dep);
}
export function trackEffects(dep) {
if (dep.has(reactiveEffect)) return;
dep.add(reactiveEffect);
reactiveEffect.deps.push(dep);
}
export function isTracking() {
return shouldTrack && reactiveEffect !== undefined;
}
export function trigger(target, key) {
let depMap = targetMap.get(target);
let dep = depMap.get(key);
triggerEffects(dep);
}
export function triggerEffects(dep) {
for (const effect of dep) {
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
}
重构之后执行所有单测,验证该重构操作是否对原有代码功能破坏,没有问题进行下一步。
那抽离出来的trackEffects
和triggerEffects
就可以用在ref
的实现中。
class RefImpl {
private _value: any;
public dep;
constructor(value) {
this._value = value;
this.dep = new Set();
}
get value() {
if (isTracking()) {
trackEffects(this.dep);
}
return this._value;
}
set value(newValue) {
this._value = newValue;
triggerEffects(this.dep);
}
}
定义一个公共属性dep
,用来存放收集到的依赖。get
时进行依赖收集,set
时先修改值再触发依赖。
此时以及实现了ref的依赖收集和触发依赖,可以执行单测进行验证,应该是无法通过的,因为我们的测试用例中还有一个点是重复赋值相同值时是不可以进行再次的依赖收集和触发依赖,这是没有实现的。
那实现上就是需要在set
时,对比新旧两个值是否相同,相同时直接返回,不触发依赖即可。
set value(newValue) {
if(Object.is(newValue, this._value)) return
this._value = newValue;
triggerEffects(this.dep);
}
每次实现完一个功能点,思考现有代码是否又可以重构优化的地方。
对于 Object.is
这样的判断,可以抽离到工具函数中,在 shared/index.ts
中导出
export function hasChanged(value, newValue) {
return !Object.is(value, newValue);
}
ref.ts
中相应修改,
set value(newValue) {
if (hasChanged(newValue, this._value)) {
this._value = newValue;
triggerEffects(this.dep);
}
}
ref
不仅仅可以用于基本类型的单值,对象数组也是可以用,只需要通过value
再访问内部属性。
it.skip("should make nested properties reactive", () => {
let data = ref({
count: 1,
});
let dummy;
effect(() => {
dummy = data.value.count;
});
expect(dummy).toBe(1);
data.value.count = 2;
expect(dummy).toBe(2);
});
需要判断传入的值是不是对象类型,如果是就走reactive
逻辑,如果不是就还剩按照上述逻辑执行。
首先需要改变的就是_value
的值,
this._value = isObject(value) ? reactive(value) : value
还有需要注意的就是,在set时对比新旧两个值,如果是对象类型,此时通过reactive
方法处理之后返回的是Proxy
,这就变成了新值newValue
是一个对象,旧值this._value
是一个Proxy
,因为需要在对比前将旧值改成Object
,可以新定义一个变量rawValue
来备份value
,对比时用rawValue
。
private _value: any;
public dep;
private rawValue: any;
constructor(value) {
this.rawValue = value;
this._value = isObject(value) ? reactive(value) : value;
this.dep = new Set();
}
set value(newValue) {
if (hasChanged(newValue, this.rawValue)) {
this.rawValue = newValue;
this._value = isObject(newValue) ? reactive(newValue) : newValue;
triggerEffects(this.dep);
}
}
可以优化的点,this._value
的赋值逻辑重复,封装一个函数来实现。
class RefImpl {
private _value: any;
public dep;
private rawValue: any;
constructor(value) {
this.rawValue = value;
this._value = convert(value);
this.dep = new Set();
}
get value() {
if (isTracking()) {
trackEffects(this.dep);
}
return this._value;
}
set value(newValue) {
if (hasChanged(newValue, this.rawValue)) {
this.rawValue = newValue;
this._value = convert(newValue);
triggerEffects(this.dep);
}
}
}
function convert(value) {
return isObject(value) ? reactive(value) : value;
}
export function ref(value) {
return new RefImpl(value);
}
ref
接受的值是单值时,可以是一个数字,也可以是布尔值,字符串,那如何知道它被get
了,有何时被set
了?Proxy
的拦截是针对于对象,这种情况下就行不通了,实现的方案就是通过对象包裹,使用class
类来实现,在类中可以定义value
值,可以实现get
set
方法,也就可以知道了何时触发get
,set
了,也就是可以进行依赖收集和触发依赖。
这样也就是 Vue3 中为何需要用ref
进行值类型的包裹,也就是为何内部需要一个.value
这样的程序设计。
项目代码仓库地址:https://github.com/Zuowendong/zwd-mini-vue