• 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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    因此在声明变量时,是使用尖括号 <> 包裹其 TS 类型,紧跟在 ref API 之后:

    // 显式指定 `msg.value` 是 `string` 类型
    const msg=ref<string>'hello'
    • 1
    • 2

    再回看该 API 本身的类型,其中使用了 T 泛型,这表示在传入函数的入参时,可以不需要手动指定其 TS 类型, TypeScript 会根据这个 API 所返回的响应式对象的 .value 属性的类型,确定当前变量的类型。
    因此也可以省略显式的类型指定,像下面这样声明变量,其类型交给 TypeScript 去自动推导:

    // TypeScript 会推导 `msg.value` 是 `string` 类型
    const msg = ref('Hello World')
    
    
    • 1
    • 2
    • 3

    对于声明时会赋予初始值,并且在使用过程中不会改变其类型的变量,是可以省略类型的显式指定的。

    而如果有显式的指定的类型,那么在一些特殊情况下,初始化时可以不必赋值,这样 TypeScript 会自动添加 undefined 类型:

    const msg = ref<string>()
    console.log(msg.value) // undefined
    
    msg.value = 'Hello World!'
    console.log(msg.value) // Hello World!
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    因为入参留空时,虽然指定了 string 类型,但实际上此时的值是 undefined ,因此实际上这个时候的 msg.value 是一个 string | undefined 的联合类型。

    对于声明时不知道是什么值,在某种条件下才进行初始化的情况,就可以省略其初始值,但是切记在调用该变量的时候对 .value 值进行有效性判断。

    而如果既不显式指定类型,也不赋予初始值,那么会被默认为 any 类型,除非真的无法确认类型,否则不建议这么做。
    API 返回值的类型
    细心的开发者还会留意到 ref API 类型里面还标注了一个返回值的 TS 类型:

    interface Ref<T> {
      value: T
    }
    
    
    • 1
    • 2
    • 3
    • 4

    它是代表整个 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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    上面这个简单的例子演示了如何手动指定 Ref 变量的类型,对于逻辑复用时的函数代码抽离、插件开发等场景非常有用!当然大部分情况下可以交给 TypeScript 自动推导,但掌握其用法,在必要的时候就派得上用场了!

    变量的定义

    在了解了如何对 Ref 变量进行类型声明之后,面对不同的数据类型,相信都得心应手了!但不同类型的值之间还是有少许差异和注意事项,例如上文提及到该 API 可以用来定义所有类型的数据,包括 Node 节点和组件,具体可以参考下文的示例。

    基本类型
    对字符串、布尔值等基本类型的定义方式,比较简单:

    //字符串
    const msg = ref<string>('123')
    //数值
    const count =ref<number>(1)
    //布尔值
    const isVip = ref<boolean>(false)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    引用类型
    对于对象、数组等引用类型也适用,比如要定义一个对象:

    // 先声明对象的格式
    interface Member {
      id: number
      name: string
    }
    
    // 在定义对象时指定该类型
    const userInfo = ref<Member>({
      id: 1,
      name: 'Tom',
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    定义一个普通数组:

    const uids =ref<number[]>([1,2,3])
    //字符串数组
    const names = ref<string[]>(['Tom', 'Petter', 'Andy'])
    
    • 1
    • 2
    • 3

    定义一个对象数组

    //声明对象的格式
    interface Member{
      id:number
      name:string
    }
    //定义一个对象数组
    const memberList = ref<Member[]>([
    	{
    	    id: 1,
    	    name: 'Tom',
    	  },
    	  {
    	    id: 2,
    	    name: 'Petter',
    	  },
    ])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    DOM 元素与子组件
    除了可以定义数据,ref 也有熟悉的用途,就是用来挂载节点,也可以挂在子组件上,也就是对应在 Vue 2 时常用的 this.$refs.xxx 获取 DOM 元素信息的作用。

    模板部分依然是熟悉的用法,在要引用的 DOM 上添加一个 ref 属性:

    <template>
      <!--DOM 元素添加 `ref` 属性 -->
      <p ref="msg">请留意该节点,有一个 ref 属性</p>
    
      <!-- 子组件也是同样的方式添加 -->
      <Child ref="child" />
    </template>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在 代码里添加的 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` 出去才可以给到 `