vue.js响应式原理(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 build src/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 在使用上的注意事项 。
创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!