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

前言

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

官方文档 中对ref的定义,

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

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

  1. 核心功能,ref包裹的对象需要 .value 访问
  2. ref包裹的对象是个响应式对象
  3. 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中,相当于与之前实现reactivetrack方法中照key来映射dep的逻辑移除了就可以了。

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

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的实现中。

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

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

项目代码仓库地址:https://github.com/Zuowendong/zwd-mini-vue

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/144921.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Failed to restart network.service: Unit network.service not found.

执行systemctl restart network命令,报错Failed to restart network.service: Unit network.service not found. 执行 yum install network-scripts命令 再次执行,正常

计算机视觉基础(6)——光流估计

前言 本章我们来学习一下图像处理基础中的运动估计。主要内容包括运动场估计和光流估计两个部分。在运动场估计中,我们将学习到运动场、光流、光流和运动场的区别;在光流估计中,我们将学习到光流估计任务、孔径问题,以及光流估计两…

μC/OS-II---计时器管理1(os_tmr.c)

目录 创建一个计时器重新启动一个计时器停止一个计时器删除一个计时器 计时器是倒计时器,当计数器达到零时执行某个动作。用户通过回调函数提供这个动作。回调函数是用户声明的函数,在计时器到期时被调用。在回调函数中绝对不能进行阻塞调用(…

腾讯云五年服务器CVM和三年轻量应用服务器选哪个?

腾讯云3年轻量和5年云服务器CVM优惠活动入口,3年轻量应用服务器配置可选2核2G4M和2核4G5M带宽,5年CVM云服务器可以选择2核4G和4核8G配置可选,阿腾云atengyun.com分享腾讯云3年轻量应用服务器和5年云服务器CVM优惠活动入口和配置报价&#xff…

【STM32单片机】比赛计时计分系统设计

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用STM32F103C8T6单片机控制器,使用OLED显示模块、矩阵按键模块、蜂鸣器等。 主要功能: 系统运行后,OLED默认显示第1节次比赛时间、AB得分。默认是12分钟倒计时…

机器学习6:逻辑回归

假设我们有一个二元分类问题,有两个特征(x1, x2)和对应的类别标签(y)。给定 以下训练数据集: 我们定义逻辑回归模型的假设函数和损失函数。假设函数使用 sigmoid 函 数来将线性函数的输出转换为概率值&…

Java之SpringCloud Alibaba【九】【Spring Cloud微服务Skywalking】

Java之SpringCloud Alibaba【一】【Nacos一篇文章精通系列】跳转Java之SpringCloud Alibaba【二】【微服务调用组件Feign】跳转Java之SpringCloud Alibaba【三】【微服务Nacos-config配置中心】跳转Java之SpringCloud Alibaba【四】【微服务 Sentinel服务熔断】跳转Java之Sprin…

【2014年数据结构真题】

41. (13分)二叉树的带权路径长度(WPL)是二叉树中所有叶结点的带权路径长度之和。 给定一棵二叉树T,采用二叉链表存储,结点结构如下: 其中叶结点的weight域保存该结点的非负权值。 设root为指向T的根结点的指针, 请设计求T 的WPL…

抖音电商的野心,中小商家的风口

文丨新熔财经 作者丨寒蝉鸣 反向消费的大浪潮,不会辜负任何一个抓住风口的平台。过去是拼多多,如今是唯品会。 靠着响应新时代消费者对“质价比”的需求,消失在大众视线许久的唯品会,不仅守住了电商老前辈的行业地位&#xff0…

Express基本接口开发-入门学习

前提推荐 任何一个新的知识都是从文档看起,因此express官方文档示例有必要去学习一遍。 推荐看: 推荐入门指南-路由指南-中间件 看完这几个内容之后心里大概知道express有些什么东西了,然后现在就可以去练习了 注意:更多示例-代…

Quarkus 替代 SpringBoot

1 概述2 SpringBoot3 Quarkus4 比较5 调查结果6 从 Spring 转换到 Quarkus7 我是 Spring 开发者,为什么要选Quarkus?8 Spring 开发者可以活用哪些现有知识?9 对Spring开发者有额外的好处吗?10 Spring开发者如何开始学习Quarkus&am…

libgdx实现雪花、下雪效果(二十三)

libgdx实现雪花、下雪效果(二十三) 转自:https://lingkang.top/archives/libgdx-shi-xian-xue-hua package effect;import com.badlogic.gdx.ApplicationAdapter; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.backends.lwjgl3.…

使用CXF调用WSDL(二)

简介 本篇文章主要解决了上篇文章中遗留的对象嵌套问题,要想全面解析无限极的对象嵌套需要使用递归去解决 上文链接: 使用CXF调用WSDL(一) 上文回顾 上文使用了单方法“ call() ”解决了List和基本类型(含String&…

基于逐次变分模态分解(SVMD)联合小波阈值去噪

代码原理 逐次变分模态分解 (Iterative Variational Mode Decomposition, IVMD) 是一种信号分解方法,它可以将一个时域信号分解为若干个本征模态函数(Intrinsic Mode Functions, IMF)。它通过迭代寻找信号的本征模态函数和残差部分&#xff…

Kalman滤波

文章目录 一、公式推导二、扩展卡尔曼滤波 卡尔曼滤波是一种最优化递归数据处理算法。(Optimal Recursive Data Processing Algorithm) Kalman滤波是时域滤波,采用状态空间描述系统,运用递推形式是计算简单,数据存储量…

TSINGSEE视频汇聚管理与AI算法视频质量检测方案

一、建设背景 随着互联网视频技术的发展,视频监管在辅助安全生产、管理等方面发挥了不可替代的作用。但是,在监管场景中,仍然存在视频掉线、视频人为遮挡、视频录像存储时长不足等问题,对企业的日常管理和运转存在较大的安全隐患…

A. Weird Sum

题目链接 : Problem - 1648A - Codeforces 题面 : 题意 : 输入 n m (1≤n*m≤1e5) 和 n 行 m 列的矩阵 a,元素范围 [1,1e5]。 对于矩阵中的所有相同元素对,即满足 a[x1][y1] a[x2][y2] 的元素对 (a[x1][y1], a[x2][y2]),把 abs(x1-x2)…

P3371 【模板】单源最短路径(弱化版)

【模板】单源最短路径(弱化版) 题目背景 本题测试数据为随机数据,在考试中可能会出现构造数据让SPFA不通过,如有需要请移步 P4779。 题目描述 如题,给出一个有向图,请输出从某一点出发到所有点的最短路…

代码随想录Day45 动态规划13 LeetCode T1143最长公共子序列 T1135 不相交的线 T53最大子数组和

LeetCode T1143 最长公共子序列 题目链接:1143. 最长公共子序列 - 力扣(LeetCode) 题目思路: 动规五部曲分析 1.确定dp数组的含义 这里dp数组的含义是结尾分别为i-1,j-1的text1和text2的最长公共子序列长度 至于为什么是i-1,j-1我之前已经说过了,这里再…

ABZ正交编码 - 异步电机常用的位置信息确定方式

什么是正交编码? ab正交编码器(又名双通道增量式编码器),用于将线性移位转换为脉冲信号。通过监控脉冲的数目和两个信号的相对相位,用户可以跟踪旋转位置、旋转方向和速度。另外,第三个通道称为索引信号&am…