vue3 源码解析(2)— ref、toRef、toRefs、shallowRef 响应式的实现

前言

vue3 源码解析(1)— reactive 响应式实现

介绍完 reactive 之后还有另一个很重要的响应式API,其中包括 ref、toRef、toRefs 和 shallowRef。这些API在vue3中起着至关重要的作用,它们帮助我们更好地管理和跟踪响应式数据的变化。本文还是通过举例子的形式逐一介绍这些API的工作原理。

ref

举个例子

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>ref</title>
</head>
<body>
<div id="app"></div>
<!--响应式模块的代码-->
<script src="../packages/reactivity/dist/reactivity.global.js"></script>
<script>let { ref, effect } = VueReactivity;let name = ref("ref");effect(() => {app.innerHTML = name.value});setTimeout(() => {name.value = "hello";}, 1000);
</script>
</body>
</html>

在这里插入图片描述
通过例子可以看到1s之后改变数据视图也跟随变,在 vue3 中是那如何实现这一效果的呢?我们先从例子中的 ref 函数出发。

实现响应式

ref 函数接收一个内部值,然后返回一个具有响应性和可变性的 ref 对象。ref 对象有一个.value属性,该属性指向内部值。换句话说,无论你传递给 ref 函数什么样的值,它都会返回一个新的对象,这个对象有一个 .value 属性,你可以通过这个属性来获取或设置原始值。

function ref (target) {return createRef(target)
}function createRef (rawValue, shallow = false) {return new RefImpl(rawValue, shallow)
}class RefImpl<T> {private readonly _v_isRef = true // 标识 refprivate _value: Tprivate _rawValue: Tconstructor (value: T, public readonly __v_isShallow: boolean) {this._rawValue = __v_isShallow ? value : toRaw(value)this._value = __v_isShallow ? value : toReactive(value)}// 类的属性访问器并进行依赖收集get value () {track(this, 'get', 'value')return this._value}set value (newVal) {const useDirectValue = this.__v_isShallowif (hasChanged(newVal, this._value)) {this._rawValue = newValthis._value = useDirectValue ? newVal : toReactive(newVal)trigger(this, 'set', 'value', newVal)}}
}

这里重点看下 RefImpl 这个类的作用是什么:

  1. RefImpl 是一个类,它用来实现 ref 函数的功能。 对象有一个 .value 属性,可以用来获取或设置原始值,并且会触发依赖收集和更新。

  2. RefImpl 的构造函数接收两个参数,value 和 __v_isShallow。value 是要转换为 ref 对象的原始值,__v_isShallow 是一个布尔值,表示是否使用浅层响应式。RefImpl 会将 value 转换为 _rawValue 和 _value 两个属性,_rawValue 是原始值的副本,_value 是原始值的响应式版本。如果 __v_isShallow 为 true,则 _rawValue 和 _value 相同,不会进行深层响应式转换。

  3. RefImpl 还定义了一个属性访问器 value,用来实现 ref 对象的 .value 属性。当读取 value 时,会调用 track 函数进行依赖收集;当设置 value 时,会判断新值和旧值是否有变化,如果有变化,则更新 _rawValue 和 _value,并调用 trigger 函数通知依赖更新。

  4. RefImpl 还有一个私有属性 _v_isRef,用来标识 ref 对象。这样可以在其他地方判断一个对象是否是 ref 对象,并进行相应的处理。

toRaw

const enum ReactiveFlags {RAW = '__v_raw'
}
interface Target {[ReactiveFlags.RAW]?: any
}function toRaw<T>(observed: T): T {const raw = observed && (observed as Target)[ReactiveFlags.RAW]return raw ? toRaw(raw) : observed
}

toRaw 函数的作用是返回一个响应式对象的原始对象。这是一个可以用来临时读取而不会产生代理访问/跟踪开销,或者写入而不会触发更改的逃生舱。

toReactive

const toReactive = <T extends unknown>(value: T): T =>isObject(value) ? reactive(value) : value

toReactive 是一个函数,它接受一个值作为参数。如果这个值是一个对象,那么 toReactive 会使用 reactive 函数将这个对象转换为响应式对象。如果这个值不是对象,那么 toReactive 就直接返回这个值。这个函数的主要作用是将一个可能是对象的值转换为响应式对象。当我们设置 ref 对象的 .value 属性时,如果新值是一个对象,那么 toReactive 会确保这个新值是响应式的,从而使得我们可以追踪这个新值的变化。

执行过程

为了更好的理解每个函数是如何执行的,我们可以通过 debugger 来调试一下。

RefImpl 类的实例

在数据更新之前 RefImpl 类的实例对应的数据如下图所示。

在这里插入图片描述

targetMap

在 effect 函数执行完成之后,此时依赖收集完之后对应的 targetMap 数据如下图所示。

在这里插入图片描述

数据更新时

执行 name.value = "hello" 更新数据时会触发 set ,此时新值和旧值不一样会触发 trigger。触发 trigger 时会从 targetMap 的子项 depsMap 中获取对应的 effect 函数执行并直接返回最新的值。这里的执行过程与之前提到的 reactive 函数执行过程类似,不了解的可以参考之前的文章。

toRef

举个例子

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>toRef</title>
</head>
<body>
<div id="app"></div>
<script src="../packages/reactivity/dist/reactivity.global.js"></script>
<script>// toRef 将目标对象中的属性值变成 reflet { reactive, toRef, effect } = VueReactivity;let state = reactive({ name: "toRef" });let name = toRef(state, "name");effect(() => {app.innerHTML = name.value;});setTimeout(() => {name.value = "hello";}, 1000);
</script>
</body>
</html>

在这里插入图片描述
同样可以看到1s之后改变数据视图也跟随变。

实现响应式

toRef 函数可以将一个响应式对象的属性转换为一个 ref 对象。这个 ref 对象会与源对象的属性保持同步,也就是说,修改源对象的属性会更新 ref 对象,反之亦然。

function toRef (target, key: string) {return new ObjectRefImpl(target, key)
}class ObjectRefImpl<T extends object, K extends keyof T> {private readonly _v_isRef = true // 标识 refconstructor (public readonly  _object: T, public readonly  _key: K) {}get value () {return this._object[this._key]}set value (newValue) {this._object[this._key] = newValue}
}

ObjectRefImpl 类有以下几个特点:

  1. 它有一个私有属性 _v_isRef ,用来标识 ref 对象。

  2. 它有两个公共属性 _object 和 _key ,分别表示源对象和属性名。

  3. 它有一个属性访问器 value ,用来实现 ref 对象的 .value 属性。当读取 value 时,会返回源对象的对应属性值;当设置 value 时,会更新源对象的对应属性值。

执行过程

ObjectRefImpl 类的实例

在数据更新之前 ObjectRefImpl 类的实例对应的数据如下图所示。

在这里插入图片描述

targetMap

因为这里的数据已经被处理成了响应式,当访问 name.value 时实质上是触发 reactive 函数中 reactiveHandlers 中 get 拦截器,所以这里不需要手动触发 track 。此时依赖收集完之后对应的 targetMap 数据如下图所示。

在这里插入图片描述

数据更新时

同理当访问 name.value = "hello" 时实质上是触发 reactive 函数中 reactiveHandlers 中 set 拦截器。所以这里也不需要手动触发 trigger ,之后的更新过程和之前类似这里不在赘述。

toRefs

举个例子

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>toRefs</title>
</head>
<body>
<div id="app"></div>
<script src="../packages/reactivity/dist/reactivity.global.js"></script>
<script>let { reactive, toRefs, effect } = VueReactivity;let state = reactive({ name: "toRef" });let { name } = toRefs(state);effect(() => {app.innerHTML = name.value;});setTimeout(() => {name.value = "hello";}, 1000);
</script>
</body>
</html>

上述代码的实现效果和上面的一致,都是在1s之后数据改变视图也改变。

实现响应式

toRefs 函数用于将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是一个 ref 对象,与源对象的对应属性保持同步。这样做的好处是,你可以在不丢失响应性的情况下,将响应式对象的属性解构到各个变量中。

function toRefs (target) {let result = isArray(target) ? new Array(target.length): {}for (let key in target) {result[key] = toRef(target, key)}return result
}

toRefs 函数做了以下几件事:

  1. 首先,它创建了一个新的空对象或数组 result ,用于存放转换后的 ref 对象。

  2. 然后,遍历 target 的每个属性。对于每个属性,它调用 toRef(target, key) 来创建一个 ref 对象,并将这个 ref 对象存放到 result 的对应属性中。这样,result[key] 就成为了一个与 target[key] 保持同步的 ref 对象。

  3. 最后,它返回 result 。这样,你就可以像操作普通对象一样操作 result ,而不用担心丢失响应性。

具体的执行过程可以参考 toRef 这里就不在赘述。

shallowRef

举个例子

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>shallowRef</title>
</head>
<body>
<div id="app"></div>
<script src="../packages/reactivity/dist/reactivity.global.js"></script>
<script>let { shallowRef, effect } = VueReactivity;const state = shallowRef({ count: 1 })effect(() => {app.innerHTML = state.value.count});setTimeout(() => {state.value.count = 2}, 1000);
</script>
</body>
</html>

与之前不同的是1s之后数据改变了但是视图却并没有更新。

实现响应式

function shallowRef (target) {return createRef(target, true)
}function createRef (rawValue, shallow = false) {return new RefImpl(rawValue, shallow) // 浅的
}

需要注意的是:

  1. 与之前创建 ref 函数不一样的是这个函数的第二个参数 true 表示创建的引用是浅的。这意味着,如果你更改了 target 的属性,vue 不会触发任何副作用或计算属性的重新计算。

  2. 如果你省略这个参数或将其设置为 false,那么 createRef 将创建一个深度引用,即 target 的所有属性都将被转换为响应式的。

  3. 如果你直接更改了 target(例如,将其设置为一个新的对象 state.value = { count: 2 }),vue 会触发响应。

执行过程

RefImpl 类的实例

在数据更新之前 RefImpl 类的实例对应的数据如下图所示。

在这里插入图片描述

数据更新时

需要注意的是执行 state.value.count = 2" 更新数据时触发的依然是 get 函数,直接返回原理的值。同时也不会触发 set函数和 effect 函数,所以这里的视图是不会进行更新的。

总结

这篇文章主要介绍了 vue 3 的响应式原理,其中涉及到了 ref、toRef、toRefs、shallowRef 等函数的实现。下面是这些函数的响应式实现的总结:

  • ref:ref 函数用于创建一个包含响应式数据的引用对象,它接受一个基本类型或对象类型的参数,并返回一个具有 value 属性的对象。当访问或修改 value 属性时,会触发响应式更新。ref 函数会对对象类型的参数进行深度响应式转换,即递归地将对象的所有属性都转换为响应式的。
  • toRef:toRef 函数用于创建一个指向另一个对象属性的响应式引用,它接受一个对象和一个属性名作为参数,并返回一个具有 value 属性的对象。当访问或修改 value 属性时,会同步地访问或修改原对象的属性,并触发响应式更新。toRef 函数不会对对象类型的属性进行深度响应式转换,即只会转换第一层属性。
  • toRefs:toRefs 函数用于将一个响应式对象转换为一个普通对象,该普通对象的每个属性都是指向原对象相应属性的响应式引用。它接受一个响应式对象作为参数,并返回一个普通对象。当访问或修改普通对象的属性时,会同步地访问或修改原对象的属性,并触发响应式更新。toRefs 函数不会对对象类型的属性进行深度响应式转换,即只会转换第一层属性。
  • shallowRef:shallowRef 函数用于创建一个浅层的响应式引用,它接受一个基本类型或对象类型的参数,并返回一个具有 value 属性的对象。当访问或修改 value 属性时,会触发响应式更新。shallowRef 函数不会对对象类型的参数进行深度响应式转换,即只会转换第一层属性。

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

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

相关文章

漏洞复现-dedecms文件上传(CVE-2019-8933)

dedecms文件上传_CVE-2019-8933 漏洞信息 Desdev DedeCMS 5.7SP2版本中存在安全漏洞CVE-2019-8933文件上传漏洞 描述 ​ Desdev DedeCMS&#xff08;织梦内容管理系统&#xff09;是中国卓卓网络&#xff08;Desdev&#xff09;公司的一套基于PHP的开源内容管理系统&#x…

【面试经典150 | 链表】合并两个有序链表

文章目录 Tag题目来源题目解读解题思路方法一&#xff1a;递归方法二&#xff1a;迭代 写在最后 Tag 【递归】【迭代】【链表】 题目来源 21. 合并两个有序链表 题目解读 合并两个有序链表。 解题思路 一种朴素的想法是将两个链表中的值存入到数组中&#xff0c;然后对数组…

轻量级仿 Spring Boot=嵌入式 Tomcat+Spring MVC

啥&#xff1f;Spring Boot 不用&#xff1f;——对。就只是使用 Spring MVC Embedded Tomcat&#xff0c;而不用 Boot。为啥&#xff1f;——因为 Boot 太重了&#xff1a;&#xff09; 那是反智吗&#xff1f;Spring Boot 好好的就只是因为太重就不用&#xff1f;——稍安勿…

有一个带头结点的单链表L,设计一个算法使其元素递增有序

有一个带头结点的单链表L&#xff0c;设计一个算法使其元素递增有序 代码思路&#xff1a; 我这里懒得搞那个指针了&#xff0c;直接遍历一遍链表&#xff0c;把链表的元素复制到数组arr里面 对数组A进行一下排序&#xff0c;排完之后再把元素复制到L里面。 至于排序你用啥算…

运维 | 使用 Docker 安装 Jenkins | Jenkins

运维 | 使用 Docker 安装 Jenkins | Jenkins 前言 本期内容主要是为了学习如何通过 Docker 安装Jenkins&#xff0c;仅作为记录与参考&#xff0c;希望对大家有所帮助。 准备工作 系统&#xff1a;CentOS 7.9配置&#xff1a;4c8g 快速安装 下面以 Docker 方式安装 Jenkin…

第2篇 机器学习基础 —(2)分类和回归

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。机器学习中的分类和回归都是监督学习的问题。分类问题的目标是将输入数据分为不同的类别&#xff0c;而回归问题的目标是预测一个连续的数值。分类问题输出的是物体所属的类别&#xff0c;而回归问题输出的是数值。本节课就…

【Matlab2016】Matlab中文版的下载、安装、激活(不建议安装过高版本!!)

这里写目录标题 首先双击R2016_win64.iso加载镜像文件双击setup.exe开始安装选择使用文件密钥安装填入密钥修改安装路径并记住此路径建议全部勾选等待安装完成 激活复制补丁到matlab路径下 创建快捷方式进入bin目录&#xff0c;找到matlab.exe 安装包 首先双击R2016_win64.iso加…

网络搭建和运维的基础题目

服务部分&#xff08;linux&#xff09; 实操部分 1.在任意文件夹下面创建形如 A/B/C/D 格式的文件夹系列。 [rootlocalhost ~]# mkdir -p A/B/C/D 2.在创建好的文件夹下面&#xff0c;A/B/C/D &#xff0c;里面创建文本文件 mkdir.txt [rootlocalhost ~]# cd A/B/C/D [r…

[17]JAVAEE-HTTP协议

目录 一、什么是HTTP协议 什么时候会用到HTTP协议&#xff1f; HTTP协议的工作流程 二、HTTP的报文格式 抓包 HTTP请求报文格式 1.首行 2.header 常见键值对&#xff1a; 3.空行 4.正文&#xff08;body&#xff09;&#xff08;有的时候可以没有&#xff09; HTTP…

ubuntu下Docker的简单使用并利用主机显示

首先分享一个docker镜像的网站&#xff1a;https://hub.docker.com/search?q 这个网站里面有很多配置好的镜像&#xff0c;可以直接拉取。 下面介绍一下docker的安装和使用。 1、docker得到安装&#xff1a; sudo apt-get install docker 2、docker拉取一个镜像到本地,这里我…

Python爬虫防止被封的方法:动态代理ip

目录 前言 一、为什么需要使用动态IP代理 1.网站反爬虫机制 2.突破本地IP限制 3.获取更多数据 二、Python爬虫动态IP代理的实现方法 1.使用第三方库 2.使用爬虫框架 三、预防被封的方法 1.代理池管理 2.请求间隔设置 3.使用多个代理 总结 前言 在进行网站爬取时&…

网络基础-3

路由开销 一条路由的开销时指到达这条路由的目的地/掩码需要付出的带价值。同一种路由协议发现有多条路由可以到达同一目的地/掩码时&#xff0c;将优选开销最小的路由&#xff0c;即只把开销最小的路由加入进本协议的路由表中。 路由协议 内部网关协议&#xff08;IGP&…

【每日一题Day361】LC2558从数量最多的堆取走礼物 | 大顶堆

从数量最多的堆取走礼物【LC2558】 给你一个整数数组 gifts &#xff0c;表示各堆礼物的数量。每一秒&#xff0c;你需要执行以下操作&#xff1a; 选择礼物数量最多的那一堆。如果不止一堆都符合礼物数量最多&#xff0c;从中选择任一堆即可。选中的那一堆留下平方根数量的礼物…

网络协议--TFTP:简单文件传送协议

15.1 引言 TFTP(Trivial File Transfer Protocol)即简单文件传送协议&#xff0c;最初打算用于引导无盘系统&#xff08;通常是工作站或X终端&#xff09;。和将在第27章介绍的使用TCP的文件传送协议&#xff08;FTP&#xff09;不同&#xff0c;为了保持简单和短小&#xff0…

C++STL----list的模拟实现

文章目录 list模拟实现的大致框架节点类的模拟实现迭代器类的模拟实现迭代器类存在的意义迭代器类的模板参数说明运算符的重载--运算符的重载&#xff01;与运算符的重载*运算符的重载->运算符的重载 list的模拟实现默认成员函数迭代器相关函数元素修改相关函数front和backi…

莫名其妙el-table不显示问题

完全复制element-ui中table代码&#xff0c;发现表格仍然不显示&#xff0c;看别人都说让降低版本&#xff0c;可我不想降低啊&#xff0c;不然其他组件有可能用不了&#xff0c;后来发现可以通过配置vite.config.js alias: {: path.resolve(__dirname, src),vue: vue/dist/vue…

Qt 实现软件启动界面动画

实现软件启动界面&#xff0c;用到QSplashScreen类。 效果 启动界面 描述 QSplashScreen小部件提供了一个可以在应用程序启动期间显示的启动画面。 启动画面通常是在应用程序启动时显示的小部件。启动画面通常用于启动时间较长的应用程序&#xff08;例如需要花费一些时间来建…

Python实现双目标定、畸变矫正、立体矫正

一&#xff0c;双目标定、畸变矫正、立体矫正的作用 双目目标定&#xff1a; 3D重建和测距&#xff1a;通过双目目标定&#xff0c;您可以确定两个摄像头之间的相对位置和朝向&#xff0c;从而能够根据视差信息计算物体的深度&#xff0c;进行三维重建和测距。姿态估计&#xf…

Ubuntu部署docker及docker常用操作

Ubuntu上安装Docker步骤&#xff1a; 更新软件包列表&#xff1a; sudo apt update安装一些必要的软件包&#xff0c;以便您可以通过HTTPS使用存储库&#xff1a; sudo apt install apt-transport-https ca-certificates curl software-properties-common添加Docker的官方GP…

CVE-2022-22963 Spring Cloud Function SpEL命令注入

一、简介 Spring Cloud Function 是基于 Spring Boot的函数计算框架。该项目致力于促进函数为主的开发单元&#xff0c;它抽象出所有传输细节和基础架构&#xff0c;并提供一个通用的模型&#xff0c;用于在各种平台上部署基于函数的软件。在Spring Cloud Function相关版本&am…