vue3的单组件的编写(三)【响应式 API 之 toRef 与 toRefs】

响应式 API 之 toRef 与 toRefs

前面讲了 ref 和 reactive 这两种响应式API ,为了方便开发者使用,vue3 还出了两个用来 reactive 转换为 ref 的API,分别是 toRef 和 toRefs 。

🌈什么是toRef 与 toRefs

这两个API看拼写能猜到,toRef 转换一个ref,toRefs 是转换所有 ref 。转换后将得到新的变量,并且新变量和原来的变量可以保持同步更新。

API作用
toRef创建一个新的 Ref 变量,转换 Reactive 对象的某个字段为 Ref 变量
toRefs创建一个新的对象,它的每个字段都是 Reactive 对象各个字段的 Ref 变量

光看概念可能不容易理解,来看下面的例子,先声明一个 reactive 变量:

interface Member {id: numbername: string
}
const userInfo : Member = reactive({id: 1 ,name: "张珊珊"
})

🌈为什么要进行转换

为什么要出这么两个 API ,官方文档没有特别说明,不过经过笔者在业务中的一些实际使用感受,可知道一些使用理由。

refreactive 在使用的过程中,各自都有不方便的地方:

ref API 虽然在 <template /> 里使用起来方便,但是在 <script /> 里进行读取 / 赋值的时候,要一直记得加上 .value ,否则 BUG 就来了。

reactive API 虽然在使用的时候,因为知道它本身是一个对象,所以不会忘记通过 foo.bar 这样的格式去操作,但是在 <template /> 渲染的时候,又因此不得不每次都使用 foo.bar 的格式去渲染。

那么有没有办法,既可以在编写 <script /> 的时候不容易出错,在写 <template /> 的时候又比较简单呢?

于是, toReftoRefs 因此诞生。

🌈toRef的使用

🚀API的TS类型和基本用法
// `toRef` API 的 TS 类型
function toRef<T extends object, K extends keyof T>(object: T,key: K,defaultValue?: T[K]
): ToRef<T[K]>// `toRef` API 的返回值的 TS 类型
type ToRef<T> = T extends Ref ? T : Ref<T>

通过接收两个必传的参数(第一个是 reactive 对象, 第二个是要转换的 key ),返回一个 Ref 变量,在适当的时候也可以传递第三个参数,为该变量设置默认值。

以上面声明好的 userInfo 为例子,如果想要转换 name 字段为 Ref 变量,需要:

const name = toRef(userInfo , 'name')
console.log(name.value)

等号左侧的 name 变量此时是一个 Ref 变量,这里因为 TypeScript 可以对其自动推导,因此声明时可以省略 TS 类型的显式指定,实际上该变量的类型是 Ref<string>

所以之后在读取和赋值时,就需要使用 name.value 来操作,在重新赋值时会同时更新 nameuserInfo.name 的值:

// 修改前先查看初始值
const name = toRef(userInfo, 'name')
console.log(name.value) // Petter
console.log(userInfo.name) // Petter// 修改 Ref 变量的值,两者同步更新
name.value = 'Tom'
console.log(name.value) // Tom
console.log(userInfo.name) // Tomty// 修改 Reactive 对象上该属性的值,两者也是同步更新
userInfo.name = 'Jerry'
console.log(name.value) // Jerry
console.log(userInfo.name) // Jerry

这个 API 也可以接收一个 Reactive 数组,此时第二个参数应该传入数组的下标:

// 这一次声明的是数组
const words = reactive(['a', 'b', 'c'])// 通过下标 `0` 转换第一个 item
const a = toRef(words, 0)
console.log(a.value) // a
console.log(words[0]) // a// 通过下标 `2` 转换第三个 item
const c = toRef(words, 2)
console.log(c.value) // c
console.log(words[2]) // c
🚀基本用法-完整代码和演示
<script lang="ts">
import { defineComponent, toRef, reactive, toRefs } from "vue"
interface Member {id: numbername: string
}
const userInfo: Member = reactive({id: 1,name: "张珊珊"
})export default defineComponent({setup() {// 在这里声明数据,或者编写函数并在这里执行它const name = toRef(userInfo, 'name');function editRefName() {name.value = '陈圆圆'}function editReactiveName() {userInfo.name = '裴南苇'}return {name,userInfo,editRefName,editReactiveName,}},
})</script><template><div><h2> ref: {{ name }}</h2><h2> reactive: {{ userInfo.name }}</h2><button @click="editRefName()">修改ref的name</button><br><button @click="editReactiveName()">修改reactive 的name</button></div>
</template><style scoped></style>

代码分别显示了 ref 和 reactive 的两种响应式的值的显示。又分别控制了,修改两个值。下面的演示结果可以看出,toRef 转换后将得到新的变量,并且新变量和原来的变量可以保持同步更新。

演示动图

🚀设置默认值

如果 Reactive 对象上有一个属性本身没有初始值,也可以传递第三个参数进行设置(默认值仅对 Ref 变量有效,即此次不会同步):

interface Member {id: numbername: stringage?: number
}const userInfo: Member = reactive({id: 2,name: "Tony"
})// 此时为了避免程序运行错误,可以指定一个初始值
// 但初始值仅对 Ref 变量有效,不会影响 Reactive 字段的值
const age = toRef( userInfo , age , 18)
console.log(age.value)  // 18
console.log(userInfo.age) // undefined// 除非重新赋值,才会使两者同时更新
age.value = 25
console.log(age.value)  // 25
console.log(userInfo.age) // 25

数组也是同理,对于可能不存在的下标,可以传入默认值避免项目的逻辑代码出现问题:

const words = reactive(['a','b','c'])
//当下标对应的值不存在时,返回 ”undefined“
const d = toRef(words,3)
console.log(d.value) // undefined
console.log(words[3]) // undefined//设置了默认后,会仅对 Ref 变量有效
cosnt e = toRef(words,4,'e')
console.log(e.value) // e
console.log(words[4]) // undefined
🚀其他用法

这个 API 还有一个特殊用法,但不建议在 TypeScript 里使用。

toRef 的过程中,如果使用了原对象上面不存在的 key ,那么定义出来的 Ref 变量的 .value 值将会是 undefined

// 众所周知, Petter 是没有女朋友的
const girlfriend = toRef(userInfo, 'girlfriend')
console.log(girlfriend.value) // undefined
console.log(userInfo.girlfriend) // undefined// 此时 Reactive 对象上只有两个 Key
console.log(Object.keys(userInfo)) // ['id', 'name']

如果对这个不存在的 key 的 Ref 变量进行赋值,那么原来的 Reactive 对象也会同步增加这个 key,其值也会同步更新。

// 赋值后,不仅 Ref 变量得到了 `Marry` , Reactive 对象也得到了 `Marry`
girlfriend.value = 'Marry'
console.log(girlfriend.value) // 'Marry'
console.log(userInfo.girlfriend) // 'Marry'// 此时 Reactive 对象上有了三个 Key
console.log(Object.keys(userInfo)) // ['id', 'name', 'girlfriend']

为什么强调不要在 TypeScript 里使用呢?因为在编译时,无法通过 TypeScript 的类型检查:

npm run build> hello-vue3@0.0.0 build
> vue-tsc --noEmit && vite buildsrc/views/home.vue:37:40 - error TS2345: Argument of type '"girlfriend"'
is not assignable to parameter of type 'keyof Member'.37     const girlfriend = toRef(userInfo, 'girlfriend')~~~~~~~~~~~~src/views/home.vue:39:26 - error TS2339: Property 'girlfriend' does not exist
on type 'Member'.39     console.log(userInfo.girlfriend) // undefined~~~~~~~~~~src/views/home.vue:45:26 - error TS2339: Property 'girlfriend' does not exist
on type 'Member'.45     console.log(userInfo.girlfriend) // 'Marry'~~~~~~~~~~Found 3 errors in the same file, starting at: src/views/home.vue:37

如果不得不使用这种情况,可以考虑使用 any 类型:

// 将该类型直接指定为 `any`
type Member = any
// 当然一般都是 `const userInfo: any`// 或者保持接口类型的情况下,允许任意键值
interface Member {[key: string]: any
}// 使用 `Record` 也是同理
type Member = Record<string, any>

但笔者还是更推荐保持良好的类型声明习惯,尽量避免这种用法。

🌈使用 toRefs

在了解了 toRef API 之后,来看看 toRefs 的用法。

🚀API 类型和基本用法

先看看它的 TS 类型:

function toRefs<T extends object>(object: T
): {[K in keyof T]: ToRef<T[K]>
}type ToRef = T extends Ref ? T : Ref<T>

与 toRef 不同, toRefs 只接收了一个参数:reactive 变量。

interface Member {id: numbername: string
}// 声明一个 Reactive 变量
const userInfo: Member = reactive({id: 1,name: 'Petter',
})// 传给 `toRefs` 作为入参
const userInfoRefs = toRefs(userInfo)

此时这个新的 userInfoRefs 变量,它的 TS 类型就不再是 Member 了,而应该是:

// 导入 `toRefs` API 的类型
import type { ToRefs } from 'vue'// 上下文代码省略...// 将原来的类型传给 API 的类型
const userInfoRefs: ToRefs<Member> = toRefs(userInfo)

也可以重新编写一个新的类型来指定它,因为每个字段都是与原来关联的 Ref 变量,所以也可以这样声明:

// 导入 `ref` API 的类型
import type { Ref } from 'vue'// 上下文代码省略...// 新声明的类型每个字段都是一个 Ref 变量的类型
interface MemberRefs {id: Ref<number>name: Ref<string>
}// 使用新的类型进行声明
const userInfoRefs: MemberRefs = toRefs(userInfo)

当然实际上日常使用时并不需要手动指定其类型, TypeScript 会自动推导,可以节约非常多的开发工作量。

toRef API 一样,这个 API 也是可以对数组进行转换:

const words = reactive(['a', 'b', 'c'])
const wordsRefs = toRefs(words)

此时新数组的类型是 Ref<string>[] ,不再是原来的 string[] 类型。

🚀解构与赋值

转换后的 Reactive 对象或数组支持 ES6 的解构,并且不会失去响应性,因为解构后的每一个变量都具备响应性。

// 为了提高开发效率,可以直接将 Ref 变量直接解构出来使用
const { name } = toRefs(userInfo)
console.log(name.value) // Petter// 此时对解构出来的变量重新赋值,原来的变量也可以同步更新
name.value = 'Tom'
console.log(name.value) // Tom
console.log(userInfo.name) // Tom

这一点和直接解构 Reactive 变量有非常大的不同,直接解构 Reactive 变量,得到的是一个普通的变量,不再具备响应性。

这个功能在使用 Hooks 函数非常好用(在 Vue 3 里也叫可组合函数, Composable Functions ),还是以一个计算器函数为例,这一次将其修改为内部有一个 Reactive 的数据状态中心,在函数返回时解构为多个 Ref 变量:

import { reactive, toRefs } from 'vue'// 声明 `useCalculator` 数据状态类型
interface CalculatorState {// 这是要用来计算操作的数据num: number// 这是每次计算时要增加的幅度step: number
}// 声明一个 “使用计算器” 的函数
function useCalculator() {// 通过数据状态中心的形式,集中管理内部变量const state: CalculatorState = reactive({num: 0,step: 10,})// 功能函数也是通过数据中心变量去调用function add() {state.num += state.step}return {...toRefs(state),add,}
}

这样在调用 useCalculator 函数时,可以通过解构直接获取到 Ref 变量,不需要再进行额外的转换工作。

// 解构出来的 `num` 和 `step` 都是 Ref 变量
const { num, step, add } = useCalculator()
console.log(num.value) // 0
console.log(step.value) // 10// 调用计算器的方法,数据也是会得到响应式更新
add()
console.log(num.value) // 10

🌈什么场景下比较适合使用它们

从便利性和可维护性来说,最好只在功能单一、代码量少的组件里使用,比如一个表单组件,通常表单的数据都放在一个对象里。

当然也可以把所有的数据都定义到一个 data 里,再去 data 里面取值,但是没有必要为了转换而转换,否则不如使用 Options API 风格。

🌈在业务中的具体运用

继续使用上文一直在使用的 userInfo 来当案例,以一个用户信息表的小 demo 做个演示。

<script /> 部分:

  1. 先用 reactive 定义一个源数据,所有的数据更新,都是修改这个对象对应的值,按照对象的写法维护数据
  2. 再通过 toRefs 定义一个给 <template /> 使用的对象,这样可以得到一个每个字段都是 Ref 变量的新对象
  3. return 的时候,对步骤 2 里的 toRefs 对象进行解构,这样导出去就是各个字段对应的 Ref 变量,而不是一整个对象
import { defineComponent, reactive, toRefs } from 'vue'interface Member {id: numbername: stringage: numbergender: string
}export default defineComponent({setup() {// 定义一个 reactive 对象const userInfo = reactive({id: 1,name: 'Petter',age: 18,gender: 'male',})// 定义一个新的对象,它本身不具备响应性,但是它的字段全部是 Ref 变量const userInfoRefs = toRefs(userInfo)// 在 2s 后更新 `userInfo`setTimeout(() => {userInfo.id = 2userInfo.name = 'Tom'userInfo.age = 20}, 2000)// 在这里解构 `toRefs` 对象才能继续保持响应性return {...userInfoRefs,}},
})

<template /> 部分:

由于 return 出来的都是 Ref 变量,所以在模板里可以直接使用 userInfo 各个字段的 key ,不再需要写很长的 userInfo.name 了。

<template><ul class="user-info"><li class="item"><span class="key">ID:</span><span class="value">{{ id }}</span></li><li class="item"><span class="key">name:</span><span class="value">{{ name }}</span></li><li class="item"><span class="key">age:</span><span class="value">{{ age }}</span></li><li class="item"><span class="key">gender:</span><span class="value">{{ gender }}</span></li></ul>
</template>

🌈需要注意的问题

请注意是否有相同命名的变量存在,比如上面在 return<template /> 使用时,在解构 userInfoRefs 的时候已经包含了一个 name 字段,此时如果还有一个单独的变量也叫 name ,就会出现渲染上的数据显示问题。

此时它们在 <template /> 里哪个会生效,取决于谁排在后面,因为 return 出去的其实是一个对象,在对象里,如果存在相同的 key ,则后面的会覆盖前面的。

下面这种情况,会以单独的 name 为渲染数据:

return {...userInfoRefs,name,
}

而下面这种情况,则是以 userInfoRefs 里的 name 为渲染数据:

return {name,...userInfoRefs,
}

所以当决定使用 toReftoRefs API 的时候,请注意这个特殊情况!

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

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

相关文章

css渐变详解(重复性线性渐变、径向渐变、重复性径向渐变的使用)

目录 线性渐变 重复性线性渐变 径向渐变 重复性径向渐变的使用 线性渐变 线性渐变是向下、向上、向左、向右、对角方向的颜色渐变。 其语法格式为&#xff1a; background-image: linear-gradient(side-or-corner|angle, linear-color-stop); 参数说明如下&#xff1a; …

物联网网关在智慧农业行业的应用案例

物联网网关在智慧农业行业的应用案例 随着科技的发展和普及&#xff0c;智慧农业已经成为了农业领域的一个重要趋势。在智慧农业中&#xff0c;物联网网关是一个非常重要的组成部分&#xff0c;它能够实现对农业设备和环境的实时监控和控制&#xff0c;从而提高农业生产效率和…

服务器不备案的影响

服务器不备案的影响 不备案&#xff0c;不能解析域名。 但凡你的域名绑定到的是国内地址&#xff0c;你不备案&#xff0c;这个域名解析未来就可能会失效。 &#xff08;你借用的其它网站的子域名情况除外&#xff0c;因为他们的网站本身主域名有可能已经备案。&#xff09; …

Linux 安装显卡驱动

Linux 安装显卡驱动

scrapy框架流程

1、Scrapy从Spider子类中提取start_url,然后构造为request请求对象 2、将request请求对象传递给爬虫中间件 3、将request请求对象传递给Scrapy引擎&#xff08;核心代码&#xff09; 4、将request请求对象传递给调度器&#xff08;它负责对多个request安排&#xff0c;好比交…

Python计算DICOM图像两点真实距离

Python计算DICOM图像两点真实距离 对比测量结果图Code对比测量结果图 DICOM阅读器(小赛看看)测量结果 python测量结果 Code import numpy as np import cv2 import math import pydicom from pydicom.pixel_data_handlers.util import convert_color_spaceds = pydicom.dc…

《第一行代码:Android》第三版-2.5.1类与对象

本文主要是创建了一个类&#xff0c;后续的很多例子都和本程序有关。 /*** You can edit, run, and share this code.* play.kotlinlang.org*/ fun main() {println("Hello, world!!!") val pPerson()p.name"Jack"p.age19p.eat() } class Person{var …

高通Camera HAL3: CamX、Chi-CDK要点

目录 一、概述 二、目录 三、CamX组件之前的关系 一、概述 高通CamX架构是高通实现的相机HAL3架构&#xff0c;被各OEM厂商广泛采用。 二、目录 代码位于vendor/qcom/proprietary下&#xff1a; camx&#xff1a;通用功能性接口的代码实现集合chi-cdk&#xff1a;可定制化…

如何正确接入API接口通过淘宝商品ID和sku ID获取到淘宝商品SKU信息接口,可获取sku价格,sku销量,sku图片及sku库存参数等

接入API接口的正确方式可能因API的具体要求而有所不同&#xff0c;但一般来说&#xff0c;以下是一些通用的步骤&#xff1a; 获取API文档&#xff1a;API文档通常包括API的请求方式、请求参数、响应格式等信息。您需要仔细阅读文档&#xff0c;了解API的具体要求和使用方式。…

MDK AC5和AC6是什么?在KEIL5中添加和选择ARMCC版本

前言 看视频有UP主提到“AC5”“AC6”这样的词&#xff0c;一开始有些不理解&#xff0c;原来他说的是ARMCC版本。 keil自带的是ARMCC5&#xff0c;由于ARMCC5已经停止维护了&#xff0c;很多开发者会选择ARMCC6。 在维护公司“成年往事”项目可能就会遇到新KEIL旧版本编译器…

springboot动态加载驱动

DynamicDriverUtil: package com.zy.fastdync.demos.web;import java.io.File; import java.net.URL; import java.net.URLClassLoader; import java.sql.*; import java.util.Properties; import java.util.logging.Logger;public class DynamicDriverUtil {// 动态加载jdbc驱…

玻色量子“揭秘”之可满足性问题(SAT)与QUBO建模

​ 摘要&#xff1a;布尔可满足性问题&#xff08;Boolean Satisfiability Problem&#xff0c;简称SAT问题&#xff09;是逻辑学和计算机科学中的一个问题&#xff0c;它的目的是确定是否存在一种解释&#xff0c;使给定的布尔公式成立。换句话说&#xff0c;它询问给定布尔公…

SpringBoot面试之SpringBoot自动装配原理

SpringBoot自动装配原理 背景 最近因为各种原因&#xff0c;我又重新加入到了找工作的大军当中。昨天在面试的时候与面试官聊到我们项目都是基于SpringBoot开发的&#xff0c;然后面试官就顺口问了句&#xff1a;”SpringBoot项目会引入许多的starter&#xff0c;比如&#x…

前端 计算机基础篇 ( 二 )

文章目录 websockt及原理ipv4和ipv6的区别线程和进程的区别cdn原理缓存所涉及的http状态码缓存的时候设置 no-store和no-cache和max-age0这几个有什么区别token一般存放在哪儿怎么设置强缓存和协商缓存强缓存&#xff1a;1. 使用 Cache-Control 头字段&#xff1a; 协商缓存&am…

C++复制构造函数中的对象形参只能是引用的形式

这是一个简单的复制构造函数的应用&#xff1a; #include <bits/stdc.h> using namespace std;class A { public:A() {cout << "创建对象&#xff08;默认构造函数&#xff09;\n";}A(A & a) { // 最好是&#xff1a;const A & acout << …

C语言打字游戏案例

#include <stdio.h> #include <stdlib.h> #include <time.h>int main() {// 设置随机数种子srand((unsigned int)time(NULL));char c[201] { 0 }; // 加上一个 \0结束符位置// 产生随机数for (int i 0; i < 20; i){c[i] rand() % 26 a;}printf("…

Flutter和Android的混合跳转

1、项目特点 项目是Flutter作为主工程&#xff0c;将Android module或SDK作为模块嵌入到flutter中&#xff0c;与通常所熟悉的Android&#xff08;或iOS&#xff09;工程将flutter 为module嵌入到工程中有所不同。 2、业务需求 任意界面间的跳转&#xff0c;不管是flutter页…

工作中死循环害死人

背景&#xff1a;研发的一段代码&#xff0c;循环一直没有跳出&#xff0c;导致其他依赖逻辑有问题&#xff0c;生产事故导致9万左右数据不正常。 这里while&#xff08;true&#xff09;真的不要轻易用 &#xff0c;后来研发改动限制mysql的id切分步长&#xff0c;控制不会有数…

去大连发展还是去苏州

公司要搬到苏州&#xff0c;你是跟随公司去苏州发展&#xff0c;还是留在大连另寻出路&#xff1f;

【RtpRtcp】1: webrtc m79:audio的ChannelReceive 创建并使用

m79中,RtpRtcp::Create 的调用很少 不知道谁负责创建ChannelReceiveclass ChannelReceive : public ChannelReceiveInterface,public MediaTransportAudioSinkInterface {接收编码后的音频帧:接收rtcp包: