浅谈响应式工具
主要记录一下常用的一些响应式工具类,比如toRef、toRefs、unref等
toRef
可以将值、refs 或 getters 规范化为 refs (3.3+)。也可以基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。
-
类型:
// 规范化签名 (3.3+) function toRef<T>( value: T ): T extends () => infer R ? Readonly<Ref<R>> : T extends Ref ? T : Ref<UnwrapRef<T>> // 对象属性签名 function toRef<T extends object, K extends keyof T>( object: T, key: K, defaultValue?: T[K] ): ToRef<T[K]> type ToRef<T> = T extends Ref ? T : Ref<T>可以看到,
toRef定义了两个函数重载- 只有一个参数的情况下:
- 如果
T是一个函数(T extends () => infer R),则返回一个只读的Ref,其值类型为函数的返回类型R - 如果
T已经是一个Ref,则直接返回T - 否则,返回一个新的
Ref,其值类型为UnwrapRef<T>,也就是说对T进行解包后的Ref。
- 如果
- 两个以上参数时:
object: T:要从中提取属性的对象key: K:要提取的属性的键。defaultValue?: T[K]:可选的默认值,如果对象中不存在该键,则使用此默认值。ToRef<T[K]>:如果属性的值已经是一个``Ref,直接返回,否则返回一个Ref`
总的来说就是,
toRef如果没有指定提取的属性键,那么就返回整个对象的Ref,否则对该键的值进行Ref后返回 - 只有一个参数的情况下:
-
用法:
const count = toRef(0); // 返回 Ref<number> const refCount = toRef(ref(1)); // 返回 Ref<number> const refCountReadonly = toRef(() => 2); // 函数时只返回函数返回值的只读 Readonly<Ref<number>> const state = { count: 0 }; const countRef = toRef(state, 'count'); // 返回 Ref<number> const countRefWithDefault = toRef(state, 'nonExistentKey', 10); // 返回 Ref<number>,值为 10 -
分析:
- 对象只是一个普通对象时,通过
toRef对该对象进行包裹
<script setup lang="ts"> import {toRef} from "vue"; // 没有使用响应式 const testData = { name : "test", } // 对整个对象进行toRef const nameRef = toRef(testData) let index:number = 0 // 点击时,对name进行修改 const handleSwitch = (): void => { index++ nameRef.value.name = `${nameRef.value.name}${index}` console.log(nameRef.value.name, testData.name) } </script> <template> <div>testData: {{testData.name}}</div> <div>name: {{nameRef.name}}</div> <el-button @click="handleSwitch">修改</el-button> </template> <style scoped> </style>可以发现,点击修改时
nameRef的name属性值,testData,也会进行同步修改,并且视图都能够进行更新。此时,将
testData使用reactive进行响应式对象,然后也会发现,与上述一致。-
对象只是一个普通对象时,通过
toRef指定对象的name<script setup lang="ts"> import {toRef} from "vue"; // 没有使用响应式 const testData = { name : "test", } // 对整个对象进行toRef const nameRef = toRef(testData, 'name') let index:number = 0 // 点击时,对name进行修改 const handleSwitch = (): void => { index++ nameRef.value = `${nameRef.value}${index}` console.log(nameRef.value, testData.name) } </script> <template> <div>testData: {{testData.name}}</div> <div>name: {{nameRef}}</div> <el-button @click="handleSwitch">修改</el-button> </template> <style scoped> </style> -
对象只是一个响应式对象时,通过
toRef指定对象的name<script setup lang="ts"> import {reactive, toRef} from "vue"; // 使用响应式 const testData = reactive({ name : "test", }) // 对整个对象进行toRef const nameRef = toRef(testData, 'name') let index:number = 0 // 点击时,对name进行修改 const handleSwitch = (): void => { index++ nameRef.value = `${nameRef.value}${index}` console.log(nameRef.value, testData.name) } </script> <template> <div>testData: {{testData.name}}</div> <div>name: {{nameRef}}</div> <el-button @click="handleSwitch">修改</el-button> </template> <style scoped> </style>可以发现,如果对象是一个普通对象时,指定了
toRef键的情况下,修改返回后的属性,源对象也会同步修改,但是视图并不会更新,如果对象本身就是一个响应式对象时,修改返回后的属性,源对象会同步修改,视图并也会更新
- 对象只是一个普通对象时,通过
-
总结:
- 只要源对象是响应式对象时,修改源对象或者
toRef的值时,两者同步更新,并更新视图 - 源对象是普通对象时,
toRef包装整个对象,修改toRef返回的值时(相当于将源对象转成响应式后返回),两者同步更新,会更新视图,反之,修改源对象时,只会同步修改,但不会更新视图 - 普通对象的情况下,
toRef如果指定了键,不管修改源对象或者toRef返回的值时,两者同步更新,但不会更新视图
- 只要源对象是响应式对象时,修改源对象或者
toRefs
将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用
toRef()创建的。
-
类型:
function toRefs<T extends object>( object: T ): { [K in keyof T]: ToRef<T[K]> } type ToRef = T extends Ref ? T : Ref<T>- 接收一个对象类型,遍历每个属性,将其转为
Ref
- 接收一个对象类型,遍历每个属性,将其转为
-
示例:
const state = { count: 0, name: 'Alice', isActive: ref(true) // 已经是一个 Ref }; const refs = toRefs(state); // 得到结果如下: const refs = { count: Ref<number>, name: Ref<string>, isActive: Ref<boolean> }; -
分析:
- 源对象是一个普通对象
<script setup lang="ts"> import {toRefs} from "vue"; const testData = { name : "test", } const nameRef = toRefs(testData); let index:number = 0 let indexRef:number = 0 const handleSwitch = (): void => { index++ // 修改源 testData.name = `${testData.name}${index}` console.log(nameRef.name.value, testData.name) } const handleSwitchToRefs = ()=>{ indexRef++ // 修改toRefs nameRef.name.value = `${nameRef.name.value}${indexRef}` console.log(nameRef.name.value, testData.name) } </script> <template> <div>testData: {{testData.name}}</div> <div>name: {{nameRef.name}}</div> <el-button @click="handleSwitch">修改源</el-button> <el-button @click="handleSwitchToRefs">修改toRefs</el-button> </template> <style scoped> </style>- 源对象是一个响应式对象
const testData = reactive({ name : "test", }) -
总结:
- 源对象是普通对象时,不论修改哪个,数据会同步更新,但是不会更新视图
- 源对象是响应式对象时,不论修改哪个,数据会同步更新,也会更新视图
toRefs是toRef的扩展,toRefs对内部所有属性转Ref,toRef是对于单个属性**(toRef(value)只是对整个对象转Ref,并不是每个属性)**
如果源对象不是一个
reactive或者shallowReactive响应式对象,控制台将警告toRefs() expects a reactive object but received a plain one.
toValue
将值、refs 或 getters 规范化为值。这与 unref() 类似,不同的是此函数也会规范化 getter 函数。如果参数是一个 getter,它将会被调用并且返回它的返回值。
这可以在组合式函数中使用,用来规范化一个可以是值、ref 或 getter 的参数。
-
类型:
function toValue<T>(source: T | Ref<T> | (() => T)): T- 顾名思义就是,获取源的原始值
-
示例:
toValue(1) // --> 1 toValue(ref(1)) // --> 1 toValue(() => 1) // --> 1
isRef
检查某个值是否为 ref。
-
类型:
function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
unref
如果参数是 ref,则返回内部值,否则返回参数本身。这是
val = isRef(val) ? val.value : val计算的一个语法糖。
-
类型:
function unref<T>(ref: T | Ref<T>): T -
示例:
unref(1) // --> 1 unref(ref(1)) // --> 1
isProxy
检查一个对象是否是由
reactive()、readonly()、shallowReactive()或shallowReadonly()创建的代理。
-
类型:
function isProxy(value: any): boolean -
示例:
import { reactive, isProxy } from 'vue'; const state = reactive({ count: 0 }); console.log(isProxy(state)); // true const plainObject = { count: 0 }; console.log(isProxy(plainObject)); // false
isReactive
检查一个对象是否是由
reactive()或shallowReactive()创建的代理。
-
类型:
function isReactive(value: unknown): boolean -
示例:
import { reactive, isReactive, readonly, isReadonly } from 'vue'; const state = reactive({ count: 0 }); const readOnlyState = readonly(state); console.log(isReactive(state)); // true console.log(isReactive(readOnlyState)); // false const plainObject = { count: 0 }; console.log(isReactive(plainObject)); // false
isReadonly
检查传入的值是否为只读对象。只读对象的属性可以更改,但他们不能通过传入的对象直接赋值。
通过
readonly()和shallowReadonly()创建的代理都是只读的,类似于没有set函数的computed()ref。
-
类型:
function isReadonly(value: unknown): boolean -
示例:
import { reactive, readonly, isReadonly } from 'vue'; const state = reactive({ count: 0 }); const readOnlyState = readonly(state); console.log(isReadonly(state)); // false console.log(isReadonly(readOnlyState)); // true const plainObject = { count: 0 }; console.log(isReadonly(plainObject)); // false
总结
toRef:将值、refs 或 getters 规范化为 refs。可以基于响应式对象的属性创建对应的 ref,修改源属性或 ref 的值会同步更新。toRefs:将响应式对象(reactive或者shallowReactive)转换为普通对象,每个属性都是指向源对象相应属性的 ref。适用于组合式函数中解构响应式对象。toValue:将值、refs 或 getters 规范化为值,适用于规范化参数。isRef:检查值是否为 ref。unref:如果参数是 ref,则返回内部值,否则返回参数本身。isProxy:检查对象是否由reactive、readonly、shallowReactive或shallowReadonly创建的代理。isReactive:检查对象是否由reactive或shallowReactive创建的代理。isReadonly:检查对象是否由readonly或shallowReadonly创建的代理。