Vue3中的 ref() 为何需要 .value ?
前言
本文是 Vue3 源码实战专栏的第 8 篇,从 0-1 实现 ref 功能函数。
官方文档 中对ref的定义,
接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value。
老规矩还是从单测入手,那ref函数的实现需要 3 个测试用例:
- 核心功能,ref包裹的对象需要 .value 访问
- ref包裹的对象是个响应式对象
- ref不仅仅可以应用在单值上,对象类型也是响应式的
ref 对象需要.value访问
单测
新建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

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 包裹对象类型是响应式
单测
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这样的程序设计。
项目代码仓库地址:github.com/Zuowendong/…
转载自:https://juejin.cn/post/7300850209637285951




