Vue3中的 ref() 为何需要 .value ?

前言

本文是 Vue3 源码实战专栏的第 8 篇,从 0-1 实现 ref 功能函数。

官方文档 中对ref的定义,

接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value

老规矩还是从单测入手,那ref函数的实现需要 3 个测试用例:

  1. 核心功能,ref包裹的对象需要 .value 访问
  2. ref包裹的对象是个响应式对象
  3. ref不仅仅可以应用在单值上,对象类型也是响应式的

ref 对象需要.value访问

单测

新建ref.spec.ts,添加第一个测试用例 happy path

scss 复制代码
it("happy path", () => {
  const original = ref(1);
  expect(original.value).toBe(1);
});

实现

新建文件 ref.ts

ref 函数接受的是一个基本类型的单值,需要将其转换成对象可以通过value来访问,可以使用class类,get语法将对象属性绑定到查询该属性时将被调用的函数。

ts 复制代码
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 包裹的对象是响应式对象

单测

ts 复制代码
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中,相当于与之前实现reactivetrack方法中照key来映射dep的逻辑移除了就可以了。

那为了代码的复用性,需要对之前effecttracktrigger进行重构。

ts 复制代码
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();
    }
  }
}

重构之后执行所有单测,验证该重构操作是否对原有代码功能破坏,没有问题进行下一步。

那抽离出来的trackEffectstriggerEffects就可以用在ref的实现中。

ts 复制代码
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时,对比新旧两个值是否相同,相同时直接返回,不触发依赖即可。

ts 复制代码
  set value(newValue) {
    if(Object.is(newValue, this._value)) return
    this._value = newValue;
    triggerEffects(this.dep);
  }

验证

重构

每次实现完一个功能点,思考现有代码是否又可以重构优化的地方。

对于 Object.is 这样的判断,可以抽离到工具函数中,在 shared/index.ts 中导出

javascript 复制代码
export function hasChanged(value, newValue) {
  return !Object.is(value, newValue);
}

ref.ts中相应修改,

kotlin 复制代码
set value(newValue) {
  if (hasChanged(newValue, this._value)) {
    this._value = newValue;
    triggerEffects(this.dep);
  }
}

ref 包裹对象类型是响应式

单测

ref 不仅仅可以用于基本类型的单值,对象数组也是可以用,只需要通过value再访问内部属性。

ts 复制代码
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的值,

ts 复制代码
this._value = isObject(value) ? reactive(value) : value

还有需要注意的就是,在set时对比新旧两个值,如果是对象类型,此时通过reactive方法处理之后返回的是Proxy,这就变成了新值newValue是一个对象,旧值this._value是一个Proxy,因为需要在对比前将旧值改成Object,可以新定义一个变量rawValue来备份value,对比时用rawValue

ts 复制代码
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的赋值逻辑重复,封装一个函数来实现。

ts 复制代码
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方法,也就可以知道了何时触发getset了,也就是可以进行依赖收集和触发依赖。

这样也就是 Vue3 中为何需要用ref进行值类型的包裹,也就是为何内部需要一个.value这样的程序设计。

项目代码仓库地址:github.com/Zuowendong/...

相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼4 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax