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

news2024/12/23 11:17:23

前言

本文是 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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

viple模拟器使用(一):线控模拟

(1)unity模拟器 通过viple程序,将viple编写逻辑运行在unity模拟器中。 首先编写viple程序,逻辑:设置一个机器人主机,并且,按↑、↓、←、→方向键的时候,能分别控制模拟机器人在unity模拟器中运行。 主机…

《视觉SLAM十四讲》-- 后端 1(上)

文章目录 08 后端 18.1 概述8.1.1 状态估计的概率解释8.1.2 线性系统和卡尔曼滤波(KF)8.1.3 非线性系统和扩展卡尔曼滤波(EKF)8.1.4 小结 08 后端 1 前端视觉里程计可以给出一个短时间内的轨迹和地图,但由于不可避免的…

基于 vue3源码 尝试 mini-vue 的实现

基于 vue3源码 尝试 mini-vue 的实现 预览&#xff1a; 1. 实现思路 渲染系统模块响应式系统mini-vue 程序入口 2. 渲染系统模块 2.1 初识 h 函数 以下是 vue 的模版语法代码&#xff1a; <template><div classcontainer>hello mini-vue</div> </…

【STM32】FreeModbus 移植Modbus-RTU从机协议到STM32详细过程

背景 FreeModbus是一款开源的Modbus协议栈&#xff0c;但是只有从机开源&#xff0c;主机源码是需要收费的。 第一步&#xff1a;下载源码 打开.embedded-experts的官网&#xff0c;连接如下&#xff1a; https://www.embedded-experts.at/en/freemodbus-downloads/ 其中给出…

【PTQ】Cross-Layer Equalization跨层均衡-证明和实践详细解读

Cross-Layer Equalization跨层均衡 aimet解读 符合规则的模型结构 统一要求&#xff1a;单数据流&#xff0c;中间激活不流向其他地方概念说明&#xff1a; Conv: gruoups1的普通卷积&#xff0c;包括TransposedConv和ConvDepthwiseConv: 深度可分离卷积&#xff0c;groupsi…

Adobe家里的“3D“建模工 | Dimension

今天&#xff0c;我们来谈谈一款在Adobe系列中比肩C4D的高级3D软件的存在—— Dimension。 Adobe Dimension &#xff0c;其定位是一款与Photoshop以及Illustrator相搭配的3D绘图软件。 Adobe Dimensions与一般的3D绘图软件相较之下&#xff0c;在操作界面在功能上有点不大相同…

第四天课程 分布式搜索引擎1

分布式搜索引擎01 – elasticsearch基础 0.学习目标 1.初识elasticsearch 1.1.了解ES 1.1.1.elasticsearch的作用 elasticsearch是一款非常强大的开源搜索引擎&#xff0c;具备非常多强大功能&#xff0c;可以帮助我们从海量数据中快速找到需要的内容 例如&#xff1a; …

常见排序算法实现

&#x1f495;"每一天都是值得被热爱的"&#x1f495; 作者&#xff1a;Mylvzi 文章主要内容&#xff1a;常见排序算法实现 1.排序的概念 所谓排序&#xff0c;就是按照特定顺序重新排列序列的操作 排序的稳定性&#xff1a; 当一个序列中存在相同的元素时 排序过…

Swift制作打包framework

新建framework项目 设置生成fat包&#xff0c;包括模拟器x86_64和arm64 Buliding Settings -> Architectures -> Build Active Architecture Only 设置为NO 设置打包环境&#xff0c;选择release edit Scheme -> run -> Build configuration 设置为 Release 设置…

专题解读|Graph Fairness代表性工作介绍

1. 图上的公平性问题 图在现实世界中无处不在&#xff0c;例如知识图谱&#xff0c;社交网络和生物网络。近年来&#xff0c;图神经网络( graph neural networks&#xff0c;GNNs ) 在图结构数据建模方面表现出了强大的能力。一般地&#xff0c;GNNs采用消息传递机制&#xff…

什么是应用集成?应用集成快速指南

什么是应用集成&#xff1f; 想象一下&#xff0c;在剧院观看音乐剧&#xff0c;没有人站在正确的地方&#xff0c;每个人都在互相交谈&#xff0c;或者有漫长而尴尬的沉默&#xff0c;管弦乐队的音乐家们在错误的时刻演奏&#xff0c;完全是混乱的&#xff0c;就会很难看。 业…

房产中介租房小程序系统开发搭建:详细指南教你如何构建

随着微信小程序的日益普及&#xff0c;越来越多的企业和个人开始尝试开发自己的小程序。以下是制作一个房地产微信小程序的详细教程&#xff0c;希望对大家有所帮助。 一、注册登录乔拓云平台&#xff0c;进入后台 首先&#xff0c;需要注册并登录乔拓云平台&#xff0c;该平台…

Centos上删除文件及目录的命令积累

01-如果我想删除Centos上当前目录下的文件 test06-2023-11-14-01.sql 该怎么操作&#xff1f; 答&#xff1a;如果你想删除CentOS上当前目录下的文件 test06-2023-11-14-01.sql&#xff0c;可以使用 rm 命令。以下是删除文件的基本语法&#xff1a; rm test06-2023-11-14-01.s…

sql查询查看数据库空间使用情况

SELECT UPPER(F.TABLESPACE_NAME) "表空间名", D.TOT_GROOTTE_MB "表空间大小(M)", D.TOT_GROOTTE_MB - F.TOTAL_BYTES "已使用空间(M)", TO_CHAR(ROUND((D.TOT_GROOTTE_MB - F.TOTAL_BYTES) / D.TOT_GROOTTE_MB * 100,2),990.99) || % "使…

Nexus的Maven私有仓库搭建

Nexus的maven私有仓库搭建 一、了解 maven仓库设置 默认设置 其中&#xff1a; maven-central: 预定义的代理Maven Central仓库&#xff0c;它包含了大量的开源Java依赖包。maven-public: 存储库是一个组合存储库&#xff0c;它包含了maven-releases和maven-snapshots存储库…

【机器学习基础】机器学习的模型评估(评估方法及性能度量原理及主要公式)

&#x1f680;个人主页&#xff1a;为梦而生~ 关注我一起学习吧&#xff01; &#x1f4a1;专栏&#xff1a;机器学习 欢迎订阅&#xff01;后面的内容会越来越有意思~ &#x1f4a1;往期推荐&#xff1a; 【机器学习基础】机器学习入门&#xff08;1&#xff09; 【机器学习基…

CM211-1 MC022主板输入刷Armbian

咋一看以为是NAND的存储&#xff0c;经过各方搜索&#xff0c;发现BWCMMQ511G08G存储芯片是狭义的NAND&#xff0c;支持emmc协议&#xff0c;故而做尝试。 烧写步骤 1.下载Armbian镜像 Armbian_23.11.0_amlogic_s905l3-cm211_lunar_6.1.60_server_2023.11.01.img.gz 2.将镜像…

Leetcode—4.寻找两个正序数组的中位数【困难】

2023每日刷题&#xff08;二十九&#xff09; Leetcode—4.寻找两个正序数组的中位数 直接法实现代码 int mid, mid1, mid2; bool findmid(int n, int k, int x) {if(n % 2 1) {if(k n / 2) {mid x;return true;}} else {if(k n / 2 - 1) {mid1 x;} else if(k n / 2) {…

程序员,你的护城河挖好了吗?

程序员的护城河 在遥远的古代&#xff0c;护城河是一种防御工事&#xff0c;通常用于保护城市或城堡免受外部攻击。它是由人工挖掘或天然形成的河流、壕沟或城墙等&#xff0c;可以作为防御屏障&#xff0c;阻止敌人的进入。 而对于程序员而言&#xff0c;“护城河”是一种比喻…

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…