likes
comments
collection
share

在Vue3 + TS项目中,获取的子组件实例如何避免每次都要判断它是否undefined

作者站长头像
站长
· 阅读数 15

问题概述

在Vue3 + TS项目中,如果我们使用ref获取子组件的实例,每次使用这个实例都需要先判断它的值是否为undefined,显得非常麻烦,有没有办法能避免这个问题,本文想探讨一下这个问题。

获取子组件实例

在Vue3的SFC中,我们通常可以使用ref来获取子组件的实例,例如:

<script setup lang="ts">
import { NesVue } from 'nes-vue'
import { ref } from 'vue'
const nes = ref()

function save() {
  nes.value.save()
}
</script>

<template>
  <nes-vue
    ref="nes"
    url="https://taiyuuki.github.io/nes-vue/Super Mario Bros (JU).nes"
  />
  <button @click="save">Save</button>
</template>

这里我导入了一个叫NesVue的组件,并通过ref获取该组件的实例,实例上有一个save方法,在button元素的点击事件中通过save函数来调用该方法。

在这个例子里,我们不需要对nes.value进行判断,TS也不会报错,因为nes.value的类型是any

在Vue3 + TS项目中,获取的子组件实例如何避免每次都要判断它是否undefined

但也因此,我们无法获得类型提示,例如上面的save方法其实是必须有参数的,但在这里不会有任何提示。

为了获得类型提示,我们需要给ref添加泛型:

import { NesVue } from 'nes-vue'
import { ref } from 'vue'
const nes = ref<InstanceType<typeof NesVue>>()

如果你还不太了解InstanceType的作用,建议看看我之前写的文章:TS类型体操(三) TS内置工具类2#InstanceType

但是加了泛型之后,TS就报错了:

在Vue3 + TS项目中,获取的子组件实例如何避免每次都要判断它是否undefined

这是因为,ref在参数留空的情况下,返回值类型是Ref<T | undefined>

export declare function ref<T = any>(): Ref<T | undefined>

这么做当然是为了避免我们在组件还没有挂载或者已经被销毁之后去使用组件的实例,如果组件未挂载或组件已销毁,它的值当然应该是undefined。

所以在每次使用组件实例前,我们不得不先判断它的值是否为undefined:

import { NesVue } from 'nes-vue'
import { ref } from 'vue'
const nes = ref<InstanceType<typeof NesVue>>()

function save() {
  if (nes.value) {
    nes.value.save('id')
  }
}

但是在这个例子中,从逻辑上来讲,我们是完全没必要事先判断的,因为button元素的点击事件,必然发生在组件挂载之后,这种判断完全是多此一举,如果仅仅只是为了防止TS的报错,我们有必要加一层if判断吗?

有没有办法能避免这个问题呢?

使用可选链

第一种解决思路是使用可选链:

import { NesVue } from 'nes-vue'
import { ref } from 'vue'
const nes = ref<InstanceType<typeof NesVue>>()

function save() {
  nes.value?.save('id')
}

写法看起来确实舒服多了,但本质上这只是延迟了问题的发生,例如,如果我们想获取实例上暴露的某个属性:

import { NesVue } from 'nes-vue'
import { ref } from 'vue'
const nes = ref<InstanceType<typeof NesVue>>()

function doSomething() {
  const width = nes.value?.width
  if (width) {
      /* do something */
  }
}

这里假设组件实例上有个width属性,如果使用可选链来获取它,它的值将是T | undefined,我们还是需要对它做非空判断。

类型断言

解决这个问题,最行之有效的办法是使用类型断言

import { NesVue } from 'nes-vue'
import { Ref, ref } from 'vue'

type NesVueInstance = InstanceType<typeof NesVue>
const nes = ref<NesVueInstance>() as Ref<NesVueInstance>

function save() {
  nes.value.save('id')
}

问题搞定,完结撒花!!

呃…就这?

封装useInstance

如果你不想每次都要手动去断言,我们可以封装一个useInstance,一劳永逸的解决这个问题,它写法非常简单,一行代码就搞定了:

// src/composables/use-instance.ts
import { Component, Ref, ref } from 'vue'

export const useInstance = <T extends abstract new (...args: any[]) => Component>() => ref() as Ref<InstanceType<T>>

然后,就可以在任意组件中使用了:

<script setup lang="ts">
import { NesVue } from 'nes-vue'
import { useInstance } from 'src/composables/use-instance'

const nes = useInstance<typeof NesVue>()

function save() {
  nes.value.save('id')
}
</script>

<template>
  <nes-vue
    ref="nes"
    url="https://taiyuuki.github.io/nes-vue/Super Mario Bros (JU).nes"
  />
  <button @click="save">Save</button>
</template>

提醒一下

需要注意的是,上述做法只是给大家提供一种解决思路,未必是完全合理的,因为这种做法会将编译时可能发生的错误延迟至运行时。

只有当你能够确保在正确的生命周期中使用组件实例,才适合这么做。