浅谈响应式工具

229 阅读6分钟

浅谈响应式工具

主要记录一下常用的一些响应式工具类,比如toReftoRefsunref

toRef

可以将值、refs 或 getters 规范化为 refs (3.3+)。也可以基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。

  1. 类型:

    // 规范化签名 (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定义了两个函数重载

    1. 只有一个参数的情况下:
      • 如果 T 是一个函数(T extends () => infer R),则返回一个只读的 Ref,其值类型为函数的返回类型 R
      • 如果 T 已经是一个 Ref,则直接返回 T
      • 否则,返回一个新的 Ref,其值类型为 UnwrapRef<T>,也就是说对T进行解包后的Ref
    2. 两个以上参数时:
      • object: T:要从中提取属性的对象
      • key: K:要提取的属性的键。
      • defaultValue?: T[K]:可选的默认值,如果对象中不存在该键,则使用此默认值。
      • ToRef<T[K]>:如果属性的值已经是一个``Ref,直接返回,否则返回一个 Ref`

    总的来说就是,toRef如果没有指定提取的属性键,那么就返回整个对象的Ref,否则对该键的值进行Ref后返回

  2. 用法:

    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
    
  3. 分析:

    • 对象只是一个普通对象时,通过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>
    
    

    recording

    可以发现,点击修改时nameRefname属性值,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>
      
      

      PixPin_2024-11-05_14-05-50

    • 对象只是一个响应式对象时,通过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>
      
      

      20241105141117

      可以发现,如果对象是一个普通对象时,指定了toRef键的情况下,修改返回后的属性,源对象也会同步修改,但是视图并不会更新,如果对象本身就是一个响应式对象时,修改返回后的属性,源对象会同步修改视图并也会更新

  4. 总结:

    • 只要源对象是响应式对象时,修改源对象或者toRef的值时,两者同步更新,并更新视图
    • 源对象是普通对象时,toRef包装整个对象,修改toRef返回的值时(相当于将源对象转成响应式后返回),两者同步更新,会更新视图,反之,修改源对象时,只会同步修改,但不会更新视图
    • 普通对象的情况下,toRef如果指定了键,不管修改源对象或者toRef返回的值时,两者同步更新,但不会更新视图

toRefs

将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。

  1. 类型:

    function toRefs<T extends object>(
      object: T
    ): {
      [K in keyof T]: ToRef<T[K]>
    }
    
    type ToRef = T extends Ref ? T : Ref<T>
    
    • 接收一个对象类型,遍历每个属性,将其转为Ref
  2. 示例:

    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>
    };
    
  3. 分析:

    • 源对象是一个普通对象
    <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>
    
    

    20241105150735

    • 源对象是一个响应式对象
    const testData = reactive({
      name : "test",
    })
    

    20241105150643

  4. 总结:

    • 源对象是普通对象时,不论修改哪个,数据会同步更新,但是不会更新视图
    • 源对象是响应式对象时,不论修改哪个,数据会同步更新,也会更新视图
    • toRefstoRef的扩展,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 的参数。

  1. 类型:

    function toValue<T>(source: T | Ref<T> | (() => T)): T
    
    • 顾名思义就是,获取源的原始值
  2. 示例:

    toValue(1) //       --> 1
    toValue(ref(1)) //  --> 1
    toValue(() => 1) // --> 1
    

isRef

检查某个值是否为 ref。

  1. 类型:

    function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
    

unref

如果参数是 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 计算的一个语法糖。

  1. 类型:

    function unref<T>(ref: T | Ref<T>): T
    
  2. 示例:

    unref(1) //       --> 1
    unref(ref(1)) //  --> 1
    

isProxy

检查一个对象是否是由 reactive()readonly()shallowReactive()shallowReadonly() 创建的代理。

  1. 类型:

    function isProxy(value: any): boolean
    
  2. 示例:

    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() 创建的代理。

  1. 类型:

    function isReactive(value: unknown): boolean
    
  2. 示例:

    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。

  1. 类型:

    function isReadonly(value: unknown): boolean
    
  2. 示例:

    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
    

总结

  1. toRef:将值、refs 或 getters 规范化为 refs。可以基于响应式对象的属性创建对应的 ref,修改源属性或 ref 的值会同步更新。
  2. toRefs:将响应式对象(reactive或者shallowReactive)转换为普通对象,每个属性都是指向源对象相应属性的 ref。适用于组合式函数中解构响应式对象。
  3. toValue:将值、refs 或 getters 规范化为值,适用于规范化参数。
  4. isRef:检查值是否为 ref。
  5. unref:如果参数是 ref,则返回内部值,否则返回参数本身。
  6. isProxy:检查对象是否由 reactivereadonlyshallowReactiveshallowReadonly 创建的代理。
  7. isReactive:检查对象是否由 reactiveshallowReactive 创建的代理。
  8. isReadonly:检查对象是否由 readonlyshallowReadonly 创建的代理。