vue3响应式API之ref的使用
作者:前端不加班
vue3响应式API之ref使用
ref 是最常用的一个响应式 API,它可以用来定义所有类型的数据,包括 Node 节点和组件。
没错,在 Vue 2 常用的 this.$refs.xxx 来取代 document.querySelector(‘.xxx’) 获取 Node 节点的方式,也是使用这个 API 来取代。
类型声明
在开始使用 API 之前,需要先了解在 TypeScript 中如何声明 Ref 变量的类型。
API 本身的类型
先看 API 本身, ref API 是一个函数,通过接受一个泛型入参,返回一个响应式对象,所有的值都通过 .value 属性获取,这是 API 本身的 TS 类型:
// `ref` API 的 TS 类型 function ref<T>(value: T): Ref<UnwrapRef<T>> // `ref` API 的返回值的 TS 类型 interface Ref<T> { value: T }
因此在声明变量时,是使用尖括号 <> 包裹其 TS 类型,紧跟在 ref API 之后:
// 显式指定 `msg.value` 是 `string` 类型 const msg=ref<string>('hello')
再回看该 API 本身的类型,其中使用了 T 泛型,这表示在传入函数的入参时,可以不需要手动指定其 TS 类型, TypeScript 会根据这个 API 所返回的响应式对象的 .value 属性的类型,确定当前变量的类型。
因此也可以省略显式的类型指定,像下面这样声明变量,其类型交给 TypeScript 去自动推导:
// TypeScript 会推导 `msg.value` 是 `string` 类型 const msg = ref('Hello World')
对于声明时会赋予初始值,并且在使用过程中不会改变其类型的变量,是可以省略类型的显式指定的。
而如果有显式的指定的类型,那么在一些特殊情况下,初始化时可以不必赋值,这样 TypeScript 会自动添加 undefined 类型:
const msg = ref<string>() console.log(msg.value) // undefined msg.value = 'Hello World!' console.log(msg.value) // Hello World!
因为入参留空时,虽然指定了 string 类型,但实际上此时的值是 undefined ,因此实际上这个时候的 msg.value 是一个 string | undefined 的联合类型。
对于声明时不知道是什么值,在某种条件下才进行初始化的情况,就可以省略其初始值,但是切记在调用该变量的时候对 .value 值进行有效性判断。
而如果既不显式指定类型,也不赋予初始值,那么会被默认为 any 类型,除非真的无法确认类型,否则不建议这么做。
API 返回值的类型
细心的开发者还会留意到 ref API 类型里面还标注了一个返回值的 TS 类型:
interface Ref<T> { value: T }
它是代表整个 Ref 变量的完整类型:
上文声明 Ref 变量时,提到的 string 类型都是指 msg.value 这个 .value 属性的类型
而 msg 这个响应式变量,其本身是 Ref 类型
如果在开发过程中需要在函数里返回一个 Ref 变量,那么其 TypeScript 类型就可以这样写(请留意 Calculator 里的 num 变量的类型):
// 导入 `ref` API import { ref } from 'vue' // 导入 `ref` API 的返回值类型 import type { Ref } from 'vue' // 声明 `useCalculator` 函数的返回值类型 interface Calculator { // 这里包含了一个 Ref 变量 num: Ref<number> add: () => void } // 声明一个 “使用计算器” 的函数 function useCalculator(): Calculator { const num = ref<number>(0) function add() { num.value++ } return { num, add, } } // 在执行使用计算器函数时,可以获取到一个 Ref 变量和其他方法 const { num, add } = useCalculator() add() console.log(num.value) // 1
上面这个简单的例子演示了如何手动指定 Ref 变量的类型,对于逻辑复用时的函数代码抽离、插件开发等场景非常有用!
当然大部分情况下可以交给 TypeScript 自动推导,但掌握其用法,在必要的时候就派得上用场了!
变量的定义
在了解了如何对 Ref 变量进行类型声明之后,面对不同的数据类型,相信都得心应手了!
但不同类型的值之间还是有少许差异和注意事项,例如上文提及到该 API 可以用来定义所有类型的数据,包括 Node 节点和组件,具体可以参考下文的示例。
基本类型
对字符串、布尔值等基本类型的定义方式,比较简单:
//字符串 const msg = ref<string>('123') //数值 const count =ref<number>(1) //布尔值 const isVip = ref<boolean>(false)
引用类型
对于对象、数组等引用类型也适用,比如要定义一个对象:
// 先声明对象的格式 interface Member { id: number name: string } // 在定义对象时指定该类型 const userInfo = ref<Member>({ id: 1, name: 'Tom', })
定义一个普通数组:
const uids =ref<number[]>([1,2,3]) //字符串数组 const names = ref<string[]>(['Tom', 'Petter', 'Andy'])
定义一个对象数组:
//声明对象的格式 interface Member{ id:number name:string } //定义一个对象数组 const memberList = ref<Member[]>([ { id: 1, name: 'Tom', }, { id: 2, name: 'Petter', }, ])
DOM 元素与子组件
除了可以定义数据,ref 也有熟悉的用途,就是用来挂载节点,也可以挂在子组件上,也就是对应在 Vue 2 时常用的 this.$refs.xxx 获取 DOM 元素信息的作用。
模板部分依然是熟悉的用法,在要引用的 DOM 上添加一个 ref 属性:
<template> <!-- 给 DOM 元素添加 `ref` 属性 --> <p ref="msg">请留意该节点,有一个 ref 属性</p> <!-- 子组件也是同样的方式添加 --> <Child ref="child" /> </template>
在 代码里添加的 ref 属性的值,是对应
请保证视图渲染完毕后再执行 DOM 或组件的相关操作(需要放到生命周期的 onMounted 或者 nextTick 函数里,这一点在 Vue 2 也是一样);
该 Ref 变量必须 return 出去才可以给到 使用,这一点是 Vue 3 生命周期的硬性要求,子组件的数据和方法如果要给父组件操作,也要 return 出来才可以。
配合上面的 ,来看看
import { defineComponent, onMounted, ref } from 'vue' import Child from '@cp/Child.vue' export default defineComponent({ components: { Child, }, setup() { // 定义挂载节点,声明的类型详见下方附表 const msg = ref<HTMLElement>() const child = ref<typeof Child>() // 请保证视图渲染完毕后再执行节点操作 e.g. `onMounted` / `nextTick` onMounted(() => { // 比如获取 DOM 的文本 console.log(msg.value.innerText) // 或者操作子组件里的数据 child.value.isShowDialog = true }) // 必须 `return` 出去才可以给到 `<template />` 使用 return { msg, child, } }, })
关于 DOM 和子组件的 TS 类型声明,可参考以下规则:
另外,关于这一小节,有一个可能会引起 TS 编译报错的情况是,一些脚手架创建出来的项目会默认启用 --strictNullChecks 选项,会导致案例中的代码无法正常编译,出现如下报错:
❯ npm run build
hello-vue3@0.0.0 build
vue-tsc --noEmit && vite buildsrc/views/home.vue:27:7 - error TS2532: Object is possibly 'undefined'.
27 child.value.isShowDialog = true
~~~~~~~~~~~
Found 1 error in src/views/home.vue:27
这是因为在默认情况下 null 和 undefined 是所有类型的子类型,但开启了 strictNullChecks 选项之后,会使 null 和 undefined 只能赋值给 void 和它们各自,这是一个更为严谨的选项,可以保障程序代码的健壮性,但对于刚接触 TypeScript 不久的开发者可能不太友好。
有以下几种解决方案可以参考:
在涉及到相关操作的时候,对节点变量增加一个判断:
// 添加 `if` 分支,判断 `.value` 存在时才执行相关代码 if (child.value) { // 读取子组件的数据 console.log(child.value.num) // 执行子组件的方法 child.value.sayHi('Use `if` in `onMounted` API.') }
通过 TS 的可选符 ? 将目标设置为可选,避免出现错误(这个方式不能直接修改子组件数据的值):
// 读取子组件的数据(留意 `.num` 前面有一个 `?` 问号) console.log(child.value?.num) // 执行子组件的方法(留意 `.sayHi` 前面有一个 `?` 问号) child.value?.sayHi('use ? in onMounted')
在项目根目录下的 tsconfig.json 文件里,显式的关闭 strictNullChecks 选项,关闭后,需要开发者在写代码的时候,自行把控好是否需要对 null 和 undefined 进行判断
{ "compilerOptions": { // ... "strictNullChecks": false } // ... }
使用 any 类型代替,但是写 TypeScript 还是尽量不要使用 any ,满屏的 AnyScript 不如直接使用 JavaScript
变量的读取与赋值
前面在介绍 API 类型的时候已经了解,通过 ref 声明的变量会全部变成对象,不管定义的是什么类型的值,都会转化为一个 Ref 对象,其中 Ref 对象具有指向内部值的单个 Property .value。
也就是说,任何 Ref 对象的值都必须通过 xxx.value 才可以正确获取。
请牢记上面这句话,初拥 Vue 3 的开发者很多 BUG 都是由于这个问题引起的(包括笔者刚开始使用 Vue 3 的那段时间,嘿嘿)。
读取变量
平时对于普通变量的值,读取的时候都是直接调用其变量名即可:
// 读取一个字符串 const msg: string = 'Hello World!' console.log(msg) // 读取一个数组 const uids: number[] = [1, 2, 3] console.log(uids[1])
而 Ref 对象的值的读取,切记!必须通过 .value !
// 读取一个字符串 const msg = ref<string>('Hello World!') console.log(msg.value) // 读取一个数组 const uids = ref<number[]>([1, 2, 3]) console.log(uids.value[1])
为变量赋值
普通变量需要使用 let 声明才可以修改其值,由于 Ref 对象是个引用类型,所以可以使用 const 声明,直接通过 .value 修改。
// 声明一个字符串变量 const msg = ref<string>('Hi!') // 等待 1s 后修改它的值 setTimeout(() => { msg.value = 'Hello!' }, 1000)
因此日常业务中,像在对接服务端 API 的接口数据时,可以自由的使用 forEach、map、filter 等方法操作 Ref 数组,或者直接重置它,而不必担心数据失去响应性。
const data = ref<string[]>([]) // 提取接口的数据 data.value = api.data.map((item: any) => item.text) // 重置数组 data.value = []
为什么突然要说这个呢?因为涉及到下一部分的知识,关于 reactive API 在使用上的注意事项。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。