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/...

相关推荐
好开心33几秒前
js高级06-ajax封装和跨域
开发语言·前端·javascript·ajax·okhttp·ecmascript·交互
小镇程序员5 分钟前
vue2 src_Todolist消息订阅版本
前端·javascript·vue.js
Zack No Bug13 分钟前
解决报错:rror: error:0308010C:digital envelope routines::unsupported
前端·javascript·vue.js
飞奔的波大爷22 分钟前
springboot vue工资管理系统源码和答辩PPT论文
vue.js·spring boot·后端
QTX1873023 分钟前
原生JS和CSS,HTML实现开屏弹窗
javascript·css·html
rhythmcc1 小时前
【GoogleChrome】在开发者工具中修改js、css并生效
开发语言·javascript·css
凌虚1 小时前
Web 端语音对话 AI 示例:使用 Whisper 和 llama.cpp 构建语音聊天机器人
前端·人工智能·后端
小宇python1 小时前
Web应用安全入门:架构搭建、漏洞分析与HTTP数据包处理
前端·安全·架构
珹洺2 小时前
从 HTML 到 CSS:开启网页样式之旅(二)—— 深入探索 CSS 选择器的奥秘
前端·javascript·css·网络·html
竺梓君2 小时前
JavaScript内存管理机制解析
javascript·全栈