原始值的响应式方案

原始值指的是 Boolean、Number、BigInt、String、Symbol、undefined 和 null 等类型的值。在JavaScript 中,原始值是按值传递的,而非按引用传递。这意味着,如果一个函数接收原始值作为参数,那么形参与实参之间没有引用关系,它们是两个完全独立的值,对形参的修改不会影响实参。另外,JavaScript 中的 Proxy 无法提供对原始值的代理,因此想要将原始值变成响应式数据,就必须对其做一层包裹,也就是我们接下来要介绍的ref。

1、引入 ref 的概念

由于 Proxy 的代理目标必须是非原始值,所以我们没有任何手段拦截对原始值的操作,例如:

01 let str = 'vue'
02 // 无法拦截对值的修改
03 str = 'vue3'

对于这个问题,我们能够想到的唯一办法是,使用一个非原始值去“包裹”原始值,例如使用一个对象包裹原始值:

01 const wrapper = {
02   value: 'vue'
03 }
04 // 可以使用 Proxy 代理 wrapper,间接实现对原始值的拦截
05 const name = reactive(wrapper)
06 name.value // vue
07 // 修改值可以触发响应
08 name.value = 'vue3'

但这样做会导致两个问题:

  • 用户为了创建一个响应式的原始值,不得不顺带创建一个包裹对象;
  • 包裹对象由用户定义,而这意味着不规范。用户可以随意命名,例如 wrapper.value、wrapper.val 都是可以的。

为了解决这两个问题,我们可以封装一个函数,将包裹对象的创建工作都封装到该函数中:

01 // 封装一个 ref 函数
02 function ref(val) {
03   // 在 ref 函数内部创建包裹对象
04   const wrapper = {
05     value: val
06   }
07   // 将包裹对象变成响应式数据
08   return reactive(wrapper)
09 }

如上面的代码所示,我们把创建 wrapper 对象的工作封装到ref 函数内部,然后使用 reactive 函数将包裹对象变成响应式数据并返回。这样我们就解决了上述两个问题。运行如下测试代码:

01 // 创建原始值的响应式数据
02 const refVal = ref(1)
03
04 effect(() => {
05   // 在副作用函数内通过 value 属性读取原始值
06   console.log(refVal.value)
07 })
08 // 修改值能够触发副作用函数重新执行
09 refVal.value = 2

上面这段代码能够按照预期工作。现在是否一切都完美了呢?并不是,接下来我们面临的第一个问题是,如何区分 refVal 到底是原始值的包裹对象,还是一个非原始值的响应式数据,如以下代码所示:

01 const refVal1 = ref(1)
02 const refVal2 = reactive({ value: 1 })

思考一下,这段代码中的 refVal1 和 refVal2 有什么区别呢?从我们的实现来看,它们没有任何区别。但是,我们有必要区分一个数据到底是不是 ref,因为这涉及下文讲解的自动脱 ref 能力。

想要区分一个数据是否是 ref 很简单,怎么做呢?如下面的代码所示:

01 function ref(val) {
02   const wrapper = {
03     value: val
04   }
05   // 使用 Object.defineProperty 在 wrapper 对象上定义一个不可枚举的属性 __v_isRef,并且值为 true
06   Object.defineProperty(wrapper, '__v_isRef', {
07     value: true
08   })
09
10   return reactive(wrapper)
11 }

我们使用 Object.defineProperty 为包裹对象 wrapper 定义了一个不可枚举且不可写的属性 __v_isRef,它的值为 true,代表这个对象是一个 ref,而非普通对象。这样我们就可以通过检查__v_isRef 属性来判断一个数据是否是 ref 了。

2、响应丢失问题

ref 除了能够用于原始值的响应式方案之外,还能用来解决响应丢失问题。首先,我们来看什么是响应丢失问题。在编写Vue.js 组件时,我们通常要把数据暴露到模板中使用,例如:

01 export default {
02   setup() {
03     // 响应式数据
04     const obj = reactive({ foo: 1, bar: 2 })
05
06     // 将数据暴露到模板中
07     return {
08       ...obj
09     }
10   }
11 }

接着,我们就可以在模板中访问从 setup 中暴露出来的数据:

01 <template>
02   <p>{{ foo }} / {{ bar }}</p>
03 </template>

然而,这么做会导致响应丢失。其表现是,当我们修改响应式数据的值时,不会触发重新渲染:

01 export default {
02   setup() {
03     // 响应式数据
04     const obj = reactive({ foo: 1, bar: 2 })
05
06     // 1s 后修改响应式数据的值,不会触发重新渲染
07     setTimeout(() => {
08       obj.foo = 100
09     }, 1000)
10
11     return {
12       ...obj
13     }
14   }
15 }

为什么会导致响应丢失呢?这是由展开运算符(…)导致的。实际上,下面这段代码:

01 return {
02   ...obj
03 }

等价于:

01 return {
02   foo: 1,
03   bar: 2
04 }

可以发现,这其实就是返回了一个普通对象,它不具有任何响应式能力。把一个普通对象暴露到模板中使用,是不会在渲染函数与响应式数据之间建立响应联系的。所以当我们尝试在一个定时器中修改 obj.foo 的值时,不会触发重新渲染。我们可以用另一种方式来描述响应丢失问题:

01 // obj 是响应式数据
02 const obj = reactive({ foo: 1, bar: 2 })
03
04 // 将响应式数据展开到一个新的对象 newObj
05 const newObj = {
06   ...obj
07 }
08
09 effect(() => {
10   // 在副作用函数内通过新的对象 newObj 读取 foo 属性值
11   console.log(newObj.foo)
12 })
13
14 // 很显然,此时修改 obj.foo 并不会触发响应
15 obj.foo = 100

如上面的代码所示,首先创建一个响应式的数据对象 obj,然后使用展开运算符得到一个新的对象 newObj,它是一个普通对象,不具有响应能力。这里的关键点在于,副作用函数内访问的是普通对象 newObj,它没有任何响应能力,所以当我们尝试修改 obj.foo 的值时,不会触发副作用函数重新执行。

如何解决这个问题呢?换句话说,有没有办法能够帮助我们实现:在副作用函数内,即使通过普通对象 newObj 来访问属性值,也能够建立响应联系?其实是可以的,代码如下:

01 // obj 是响应式数据
02 const obj = reactive({ foo: 1, bar: 2 })
03
04 // newObj 对象下具有与 obj 对象同名的属性,并且每个属性值都是一个对象,
05 // 该对象具有一个访问器属性 value,当读取 value 的值时,其实读取的是 obj 对象下相应的属性值
06 const newObj = {
07   foo: {
08     get value() {
09       return obj.foo
10     }
11   },
12   bar: {
13     get value() {
14       return obj.bar
15     }
16   }
17 }
18
19 effect(() => {
20   // 在副作用函数内通过新的对象 newObj 读取 foo 属性值
21   console.log(newObj.foo.value)
22 })
23
24 // 这时能够触发响应了
25 obj.foo = 100

在上面这段代码中,我们修改了 newObj 对象的实现方式。可以看到,在现在的 newObj 对象下,具有与 obj 对象同名的属性,而且每个属性的值都是一个对象,例如 foo 属性的值是:

01 {
02   get value() {
03     return obj.foo
04   }
05 }

该对象有一个访问器属性 value,当读取 value 的值时,最终读取的是响应式数据 obj 下的同名属性值。也就是说,当在副作用函数内读取 newObj.foo 时,等价于间接读取了 obj.foo 的值。这样响应式数据自然能够与副作用函数建立响应联系。于是,当我们尝试修改 obj.foo 的值时,能够触发副作用函数重新执行。

观察 newObj 对象,可以发现它的结构存在相似之处:

01 const newObj = {
02   foo: {
03     get value() {
04       return obj.foo
05     }
06   },
07   bar: {
08     get value() {
09       return obj.bar
10     }
11   }
12 }

foo 和 bar 这两个属性的结构非常像,这启发我们将这种结构抽象出来并封装成函数,如下面的代码所示:

01 function toRef(obj, key) {
02   const wrapper = {
03     get value() {
04       return obj[key]
05     }
06   }
07
08   return wrapper
09 }

toRef 函数接收两个参数,第一个参数 obj 是一个响应式数据,第二个参数是 obj 对象的一个键。该函数会返回一个类似于 ref 结构的 wrapper 对象。有了 toRef 函数后,我们就可以重新实现 newObj 对象了:

01 const newObj = {
02   foo: toRef(obj, 'foo'),
03   bar: toRef(obj, 'bar')
04 }

可以看到,代码变得非常简洁。但如果响应式数据 obj 的键非常多,我们还是要花费很大力气来做这一层转换。为此,我们可以封装 toRefs 函数,来批量地完成转换:

01 function toRefs(obj) {
02   const ret = {}
03   // 使用 for...in 循环遍历对象
04   for (const key in obj) {
05     // 逐个调用 toRef 完成转换
06     ret[key] = toRef(obj, key)
07   }
08   return ret
09 }

现在,我们只需要一步操作即可完成对一个对象的转换:

01 const newObj = { ...toRefs(obj) }

可以使用如下代码进行测试:

01 const obj = reactive({ foo: 1, bar: 2 })
02
03 const newObj = { ...toRefs(obj) }
04 console.log(newObj.foo.value) // 1
05 console.log(newObj.bar.value) // 2

现在,响应丢失问题就被我们彻底解决了。解决问题的思路是,将响应式数据转换成类似于 ref 结构的数据。但为了概念上的统一,我们会将通过 toRef 或 toRefs 转换后得到的结果视为真正的 ref 数据,为此我们需要为 toRef 函数增加一段代码:

01 function toRef(obj, key) {
02   const wrapper = {
03     get value() {
04       return obj[key]
05     }
06   }
07   // 定义 __v_isRef 属性
08   Object.defineProperty(wrapper, '__v_isRef', {
09     value: true
10   })
11
12   return wrapper
13 }

可以看到,我们使用 Object.defineProperty 函数为 wrapper 对象定义了 __v_isRef 属性。这样,toRef 函数的返回值就是真正意义上的 ref 了。通过上述讲解我们能注意到,ref 的作用不仅仅是实现原始值的响应式方案,它还用来解决响应丢失问题。

但上文中实现的 toRef 函数存在缺陷,即通过 toRef 函数创建的 ref 是只读的,如下面的代码所示:

01 const obj = reactive({ foo: 1, bar: 2 })
02 const refFoo = toRef(obj, 'foo')
03
04 refFoo.value = 100 // 无效

这是因为 toRef 返回的 wrapper 对象的 value 属性只有getter,没有 setter。为了功能的完整性,我们应该为它加上setter 函数,所以最终的实现如下:

01 function toRef(obj, key) {
02   const wrapper = {
03     get value() {
04       return obj[key]
05     },
06     // 允许设置值
07     set value(val) {
08       obj[key] = val
09     }
10   }
11
12   Object.defineProperty(wrapper, '__v_isRef', {
13     value: true
14   })
15
16   return wrapper
17 }

可以看到,当设置 value 属性的值时,最终设置的是响应式数据的同名属性的值,这样就能正确地触发响应了。

3、自动脱 ref

toRefs 函数的确解决了响应丢失问题,但同时也带来了新的问题。由于 toRefs 会把响应式数据的第一层属性值转换为 ref,因此必须通过 value 属性访问值,如以下代码所示:

01 const obj = reactive({ foo: 1, bar: 2 })
02 obj.foo // 1
03 obj.bar // 2
04
05 const newObj = { ...toRefs(obj) }
06 // 必须使用 value 访问值
07 newObj.foo.value // 1
08 newObj.bar.value // 2

这其实增加了用户的心智负担,因为通常情况下用户是在模板中访问数据的,例如:

01 <p>{{ foo }} / {{ bar }}</p>

用户肯定不希望编写下面这样的代码:

01 <p>{{ foo.value }} / {{ bar.value }}</p>

因此,我们需要自动脱 ref 的能力。所谓自动脱 ref,指的是属性的访问行为,即如果读取的属性是一个 ref,则直接将该 ref 对应的 value 属性值返回,例如:

01 newObj.foo // 1

可以看到,即使 newObj.foo 是一个 ref,也无须通过newObj.foo.value 来访问它的值。要实现此功能,需要使用Proxy 为 newObj 创建一个代理对象,通过代理来实现最终目标,这时就用到了上文中介绍的 ref 标识,即 __v_isRef 属性,如下面的代码所示:

01 function proxyRefs(target) {
02   return new Proxy(target, {
03     get(target, key, receiver) {
04       const value = Reflect.get(target, key, receiver)
05       // 自动脱 ref 实现:如果读取的值是 ref,则返回它的 value 属性值
06       return value.__v_isRef ? value.value : value
07     }
08   })
09 }
10
11 // 调用 proxyRefs 函数创建代理
12 const newObj = proxyRefs({ ...toRefs(obj) })

在上面这段代码中,我们定义了 proxyRefs 函数,该函数接收一个对象作为参数,并返回该对象的代理对象。代理对象的作用是拦截 get 操作,当读取的属性是一个 ref 时,则直接返回该 ref 的 value 属性值,这样就实现了自动脱 ref:

01 console.log(newObj.foo) // 1
02 console.log(newObj.bar) // 2

实际上,我们在编写 Vue.js 组件时,组件中的 setup 函数所返回的数据会传递给 proxyRefs 函数进行处理:

01 const MyComponent = {
02   setup() {
03     const count = ref(0)
04
05     // 返回的这个对象会传递给 proxyRefs
06     return { count }
07   }
08 }

这也是为什么我们可以在模板直接访问一个 ref 的值,而无须通过 value 属性来访问:

01 <p>{{ count }}</p>

既然读取属性的值有自动脱 ref 的能力,对应地,设置属性的值也应该有自动为 ref 设置值的能力,例如:

01 newObj.foo = 100 // 应该生效

实现此功能很简单,只需要添加对应的 set 拦截函数即可:

01 function proxyRefs(target) {
02   return new Proxy(target, {
03     get(target, key, receiver) {
04       const value = Reflect.get(target, key, receiver)
05       return value.__v_isRef ? value.value : value
06     },
07     set(target, key, newValue, receiver) {
08       // 通过 target 读取真实值
09       const value = target[key]
10       // 如果值是 Ref,则设置其对应的 value 属性值
11       if (value.__v_isRef) {
12         value.value = newValue
13         return true
14       }
15       return Reflect.set(target, key, newValue, receiver)
16     }
17   })
18 }

如上面的代码所示,我们为 proxyRefs 函数返回的代理对象添加了 set 拦截函数。如果设置的属性是一个 ref,则间接设置该ref 的 value 属性的值即可。

实际上,自动脱 ref 不仅存在于上述场景。在 Vue.js 中,reactive 函数也有自动脱 ref 的能力,如以下代码所示:

01 const count = ref(0)
02 const obj = reactive({ count })
03
04 obj.count // 0

可以看到,obj.count 本应该是一个 ref,但由于自动脱 ref 能力的存在,使得我们无须通过 value 属性即可读取 ref 的值。这么设计旨在减轻用户的心智负担,因为在大部分情况下,用户并不知道一个值到底是不是 ref。有了自动脱 ref 的能力后,用户在模板中使用响应式数据时,将不再需要关心哪些是 ref,哪些不是 ref。

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

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

相关文章

代码逻辑修复与其他爬虫ip库的应用

在一个项目中&#xff0c;由于需要设置 http_proxy 来爬虫IP访问网络&#xff0c;但在使用 requests 库下载文件时遇到了问题。具体表现为在执行 Python 脚本时&#xff0c;程序会阻塞并最终超时&#xff0c;无法正常完成文件下载。 解决方案 针对这个问题&#xff0c;我们可以…

VB.net读写S50/F08IC卡,修改卡片密码控制位源码

本示例使用设备&#xff1a;Android Linux RFID读写器NFC发卡器WEB可编程NDEF文本/智能海报/-淘宝网 (taobao.com) 函数声明 Module Module1读卡函数声明Public Declare Function piccreadex Lib "OUR_MIFARE.dll" (ByVal ctrlword As Byte, ByRef serial As Byte, …

基于单片机K型热电偶温度采集报警系统

**单片机设计介绍&#xff0c; 基于单片机K型热电偶温度采集报警系统 文章目录 一 概要简介系统特点系统组成工作原理应用领域 二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 # 基于单片机K型热电偶温度采集报警系统介绍 简介 该系统是基于单片…

MongoDB归并连续号段-(待验证)

实现按照不同条件归并连续号段的方式与具体的数据模型和查询需求有关&#xff0c;以下是一种常见的方式&#xff1a; 假设有一个文档集合&#xff0c;包含如下字段&#xff1a; {"_id": ObjectId("613c3050d5d9b45a0de7c290"),"group": "…

【论文阅读笔记】Deep learning for time series classification: a review

【论文阅读笔记】Deep learning for time series classification: a review 摘要 在这篇文章中&#xff0c;作者通过对TSC的最新DNN架构进行实证研究&#xff0c;探讨了深度学习算法在TSC中的当前最新性能。文章提供了对DNNs在TSC的统一分类体系下在各种时间序列领域中的最成功…

8 Redis与Lua

LUA脚本语言是C开发的&#xff0c;类似存储过程,是为了实现完整的原子性操作&#xff0c;可以用来补充redis弱事务的缺点. 1、LUA脚本的好处 2、Lua脚本限流实战 支持分布式 import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis…

Map 和 WeakMap:JavaScript 中的键值对集合

JavaScript 是一种动态、弱类型的脚本语言&#xff0c;经常用于构建现代 Web 应用程序。在编写 JavaScript 代码时&#xff0c;我们经常需要使用各种数据结构来存储和管理数据。其中&#xff0c;Map 和 WeakMap 就是两个非常有用的数据结构&#xff0c;它们分别提供了用于存储键…

Excel 文件比较工具 xlCompare 11.01 Crack

比较两个 Excel 文件之间的差异 xlCompare. xlCompare.com 是性能最佳的 Excel diff 工具&#xff0c;用于比较两个 Excel 文件或工作表并在线突出显示差异。xlCompare 包括免费的在线 Excel 和 CSV 文件比较服务以及用于比较和合并 Excel 文件的强大桌面工具。如果您想在线了…

软件测试: 测试用例

一. 软件测试四要素 测试环境,操作步骤,测试数据,预期结果 二. 基于需求进行测试用例的设计 基于需求设计测试用例是测试设计和开发测试用例的基础,第一步就要分析测试需求,验证需求是否正确,完整,无二义性,并且逻辑自洽.在需求正确的基础上细化测试需求,从测试需求提炼出一…

Java语言基础第三天

运算符&#xff1a; 算术&#xff1a;、-、*、/、%、、-- %:取模/取余&#xff0c;余数为0即为整除 System.out.println(5%2); //1&#xff0c;商2余1 System.out.println(8%2); //0&#xff0c;商4余0----整除 System.out.println(2%8); //2&#xff0c;商0余2 /--:自增1/自…

React整理总结(五、Redux)

1.Redux核心概念 纯函数 确定的输入&#xff0c;一定会产生确定的输出&#xff1b;函数在执行过程中&#xff0c;不能产生副作用 store 存储数据 action 更改数据 reducer 连接store和action的纯函数 将传入的state和action结合&#xff0c;生成一个新的state dispatc…

2023年亚太杯数学建模亚太赛A题思路解析+代码+论文

下文包含&#xff1a;2023年亚太杯数学建模亚太赛A题思路解析代码参考论文等及如何准备数学建模竞赛&#xff08;23号比赛开始后逐步更新&#xff09; C君将会第一时间发布选题建议、所有题目的思路解析、相关代码、参考文献、参考论文等多项资料&#xff0c;帮助大家取得好成…

猫12分类:使用yolov5训练检测模型

前言&#xff1a; 在使用yolov5之前&#xff0c;尝试过到百度飞桨平台&#xff08;小白不建议&#xff09;、AutoDL平台&#xff08;这个比较友好&#xff0c;经济实惠&#xff09;训练模型。但还是没有本地训练模型来的舒服。因此远程了一台学校电脑来搭建自己的检测模型。配置…

hdfsClient_java对hdfs进行上传、下载、删除、移动、打印文件信息尚硅谷大海哥

Java可以通过Hadoop提供的HDFS Java API来控制HDFS。通过HDFS Java API&#xff0c;可以实现对HDFS的文件操作&#xff0c;包括文件的创建、读取、写入、删除等操作。 具体来说&#xff0c;Java可以通过HDFS Java API来创建一个HDFS文件系统对象&#xff0c;然后使用该对象来进…

Django DRF权限组件

在Django的drf框架内的权限组件&#xff0c;如果遇到多个权限认证类&#xff0c;是需要所有的权限类都要通过验证&#xff0c;才能访问视图。 一、简单示例 1、per.py 自定义权限类 from rest_framework.permissions import BasePermission import randomclass MyPerssion(B…

傅里叶级数公式及其收敛问题

文章目录 abstract函数展开成傅里叶系数傅里叶系数求解 a 0 a_0 a0​求解 a n a_n an​求解 b n b_n bn​小结 傅里叶级数&#x1f388;周期为 2 π 2\pi 2π的函数的fourier级数展开公式小结三角级数收敛问题Dirichlet收敛定理例 abstract 傅里叶级数公式及其收敛问题介绍周期…

自动化运维中间件架构概况

自动化运维中间件架构概况 kubernetesjenkins 安装k8s后 设置 Jenkins 任务: 在 Jenkins 中创建一个新的任务&#xff1a; 配置源代码管理&#xff1a;选择 Git&#xff0c;并提供 GitLab 仓库的 URL、凭据和分支信息。配置构建步骤&#xff1a;选择 Maven 构建&#xff0c;…

集合的自反关系和对称关系

集合的自反关系和对称关系 一&#xff1a;集合的自反关系1&#xff1a;原理&#xff1a;2&#xff1a;代码实现 二&#xff1a;对称关系1&#xff1a;原理&#xff1a;2&#xff1a;代码实现 三&#xff1a;总结 一&#xff1a;集合的自反关系 1&#xff1a;原理&#xff1a; …

【python】直方图正则化详解和示例

直方图正则化&#xff08;Histogram Normalization&#xff09;是一种图像增强技术&#xff0c;目的是改变图像的直方图以改善图像的质量。具体来说&#xff0c;它通过将图像的直方图调整为指定的形状&#xff0c;以增强图像的对比度和亮度。 直方图正则化的基本步骤如下&…

【Android Jetpack】Hilt的理解与浅析

文章目录 依赖注入DaggerHiltKoin添加依赖项Hilt常用注解的含义HiltAndroidAppAndroidEntryPointInjectModuleInstallInProvidesEntryPoint Hilt组件生命周期和作用域如何使用 Hilt 进行依赖注入 本文只是进行了简单入门&#xff0c;博客仅当做笔记用。 依赖注入 依赖注入是一…