Code前端首页关于Code前端联系我们

Vue3里怎么用ref获取子组件?

terry 2周前 (10-01) 阅读数 42 #Vue

在Vue3开发中,不少同学会碰到需要获取子组件实例或者操作子组件方法、数据的场景,比如父组件要调用子组件的弹窗方法、获取子组件里的表单数据,这时候用ref来获取子组件就成了关键操作,但刚接触Vue3的话,很容易在用法上踩坑,今天就一步步拆解“Vue3里怎么用ref获取子组件”这个问题,从基础到进阶全讲明白~

基础用法:给子组件加ref,父组件咋拿到?

先理解核心逻辑:Vue3里,父组件要通过ref获取子组件,得做两件事——给子组件标签加ref属性子组件用defineExpose暴露要给父组件访问的内容,最后父组件用ref变量接收子组件实例。

举个实际例子,假设子组件里有个打开弹窗的方法和一段文本,父组件要调用方法、读文本:

子组件(ChildComp.vue)代码:

<template>
  <div>{{ message }}</div>
</template>
<script setup>
import { ref } from 'vue'
// 子组件内部的方法和数据
const message = ref('子组件消息')
const openDialog = () => {
  console.log('弹窗打开啦~')
  // 这里写实际打开弹窗的逻辑,比如显示遮罩层
}
// 关键!用defineExpose把要给父组件用的内容暴露出去
defineExpose({
  openDialog,
  message
})
</script>

父组件代码:

<template>
  <ChildComp ref="childRef" />
  <button @click="handleCall">调用子组件方法</button>
</template>
<script setup>
import { ref } from 'vue'
import ChildComp from './ChildComp.vue'
// 定义ref变量,初始值设为null(因为组件挂载后Vue才会赋值)
const childRef = ref(null) 
const handleCall = () => {
  // 要先判断childRef.value是否存在(避免组件还没挂载就访问)
  if (childRef.value) { 
    childRef.value.openDialog() // 调用子组件暴露的方法
    console.log(childRef.value.message) // 读取子组件暴露的数据
  }
}
</script>

这里有几个关键点要注意:

  • 子组件用了<script setup>语法时,默认不对外暴露内部变量/方法,必须用defineExpose主动“开门”;
  • 父组件的ref变量要初始化为null,因为Vue是在组件挂载后才会把实例赋值给ref;
  • 访问childRef.value时一定要做非空判断(比如if (childRef.value)),否则页面加载瞬间子组件还没渲染,直接访问会报错。

为什么必须用defineExpose?不暴露能拿到吗?

这得从Vue3的<script setup>设计说起——它默认是“封闭模式”,为了让组件内部逻辑更封装,防止外部随意访问,打个比方,<script setup>里的变量和方法,就像手机里的私密相册,默认上锁;defineExpose分享按钮”,点了之后才能把指定内容给外部看。

如果子组件没写defineExpose,父组件里的childRef.value能拿到啥?只能拿到Vue内置的一些实例属性(比如$el,对应组件的DOM元素),但自己写的messageopenDialog这些完全拿不到

对比Vue2就更明显了:Vue2里只要子组件用选项式API(比如methodsdata),父组件this.$refs.xxx能直接拿到所有方法和数据;但Vue3的<script setup>为了更严格的封装,把“暴露”变成了主动操作——必须用defineExpose,这也是很多从Vue2转过来的同学最容易踩的坑!

v-for循环里的多个子组件,怎么用ref获取?

实际开发中,经常需要循环渲染多个子组件(比如列表里的每个项都是子组件),这时候要给每个子组件加ref,还要统一管理它们的实例,做法很简单:把ref绑定到数组,Vue会自动把每个子组件实例塞进数组里

举个例子,父组件循环渲染3个子组件,每个子组件有个“打印内容”的方法,父组件要一键调用所有子组件的方法:

子组件(ChildItem.vue)代码:

<template>
  <div>{{ item.name }}</div>
</template>
<script setup>
import { defineProps } from 'vue'
const props = defineProps(['item'])
// 暴露给父组件的方法
const printInfo = () => {
  console.log(`我是${props.item.name}`)
}
defineExpose({
  printInfo
})
</script>

父组件代码:

<template>
  <div v-for="(item, index) in list" :key="index">
    <ChildItem ref="childRefs" :item="item" />
  </div>
  <button @click="callAll">调用所有子组件方法</button>
</template>
<script setup>
import { ref, reactive } from 'vue'
import ChildItem from './ChildItem.vue'
// 初始化一个空数组,Vue会自动把每个子组件实例放进来
const childRefs = ref([]) 
const list = reactive([
  { id: 1, name: '组件1' },
  { id: 2, name: '组件2' },
  { id: 3, name: '组件3' }
])
const callAll = () => {
  // 遍历数组,逐个调用子组件方法
  childRefs.value.forEach(child => {
    if (child) { // 同样要判断是否存在
      child.printInfo()
    }
  })
}
</script>

这里的关键是:父组件的ref变量要初始化为空数组,Vue在渲染v-for列表时,会自动把每个子组件的实例push进这个数组,这样父组件就能通过数组下标或者遍历,操作每个子组件啦~

TypeScript环境下,怎么给ref加类型?

如果项目用了TypeScript,为了让代码有类型提示、避免报错,得给ref指定子组件暴露内容的类型,步骤分两步:子组件定义暴露的类型,父组件获取并使用这个类型。

子组件(ChildComp.ts)代码(用TS):

<template>
  <div>{{ message }}</div>
</template>
<script setup lang="ts">
import { ref, Ref } from 'vue'
// 定义子组件要暴露的类型
type Exposed = {
  openDialog: () => void
  message: string
}
const message = ref('子组件消息') as Ref<string>
const openDialog = () => {
  console.log('弹窗打开')
}
// 用defineExpose<Exposed>指定暴露的类型
defineExpose<Exposed>({
  openDialog,
  message
})
</script>

父组件代码(用TS):

<template>
  <ChildComp ref="childRef" />
  <button @click="handleCall">调用方法</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import ChildComp from './ChildComp.vue'
// 获取子组件暴露的类型:用InstanceType<typeof 子组件>['exposed']
type ChildExposed = InstanceType<typeof ChildComp>['exposed']
// 给ref指定类型:ChildExposed | null(因为初始是null)
const childRef = ref<ChildExposed | null>(null) 
const handleCall = () => {
  // TS会自动提示childRef.value的类型,写错方法名会报错
  childRef.value?.openDialog() 
}
</script>

这样写的好处是:父组件里访问子组件的方法/数据时,TS能帮我们做类型检查,比如如果子组件暴露的是openDialog,但父组件写成openDialogg(多了个g),TS会直接标红报错,避免运行时才发现问题~

常见错误:这些坑你可能踩过

就算理解了基础用法,实际开发中还是容易碰到报错,这里总结几个高频问题和解决办法:

报错“Cannot read properties of null (reading 'xxx')”

原因:子组件还没挂载,就去访问ref.value里的内容,比如父组件一加载就执行childRef.value.xxx,但这时候子组件可能还在渲染中,childRef.value还是null

解决:

  • 给访问逻辑加非空判断(if (childRef.value));
  • 把调用逻辑放到“确定子组件已挂载”的时机,比如点击按钮时(用户主动触发,这时候子组件肯定已经渲染了),或者用onMounted钩子(但要注意父组件和子组件的挂载顺序,有时候父组件mounted时子组件还没挂载完,所以更稳妥的是用事件触发)。

子组件用了keep-alive,切换后ref失效

场景:子组件被<KeepAlive>缓存,切换路由或标签后,再回来发现ref获取的实例不对,或者方法调用没反应。

原因:keep-alive会缓存组件实例,切换时组件可能处于“激活/停用”状态,ref的绑定逻辑需要适配生命周期。

解决:

  • 在子组件的activated钩子(组件被激活时触发)里,重新初始化或更新ref相关逻辑;
  • 父组件里监听子组件的激活状态,再处理ref的调用。

忘记写defineExpose,父组件拿不到内容

这是最最最常见的!尤其是刚用Vue3的同学,写完子组件方法就以为父组件能直接拿,结果发现childRef.value里啥都没有。

解决:检查子组件是否用defineExpose把需要的方法、数据暴露出去,别漏了这一步~

进阶:和组合式API结合,抽离ref逻辑

如果父组件里操作子组件ref的逻辑很复杂(比如要调用多个方法、处理异步逻辑),可以把这些逻辑抽成组合式函数,让代码更简洁、好复用。

举个例子,父组件要调用子组件的“打开弹窗”和“获取表单数据”两个方法,把这部分逻辑抽到useChildLogic.ts里:

组合式函数(useChildLogic.ts):

import { ref } from 'vue'
export function useChildLogic() {
  const childRef = ref(null)
  // 封装调用子组件方法的逻辑
  const callOpenDialog = () => {
    if (childRef.value) {
      childRef.value.openDialog()
    }
  }
  // 封装获取子组件表单数据的逻辑
  const getFormData = () => {
    if (childRef.value) {
      return childRef.value.formData
    }
    return null
  }
  return {
    childRef,
    callOpenDialog,
    getFormData
  }
}

父组件使用:

<template>
  <ChildComp ref="childRef" />
  <button @click="callOpenDialog">打开弹窗</button>
  <button @click="logFormData">打印表单数据</button>
</template>
<script setup>
import { useChildLogic } from './useChildLogic'
// 直接用组合式函数,逻辑全在里面
const { childRef, callOpenDialog, getFormData } = useChildLogic()
const logFormData = () => {
  console.log(getFormData())
}
</script>

这样做的好处是:父组件不用关心ref的细节,所有和子组件交互的逻辑都被封装到组合式函数里,如果有多个父组件需要和子组件交互,直接复用这个函数就行,代码维护性拉满~

Vue2和Vue3获取子组件ref的差异

最后对比下Vue2和Vue3的区别,帮从Vue2迁移的同学快速理解:

场景 Vue2做法 Vue3做法(