在 Vue 的组件通信模型中,数据流通常是自上而下的(Props 向下,Events 向上)。但有时父组件确实需要直接触发子组件内部的某个行为——例如重置表单、打开弹窗、调用子组件封装好的数据刷新方法。这时就需要用到 ref 引用 和 defineExpose。
基本用法:ref 获取子组件实例
Vue 允许父组件通过 ref 属性标记子组件,然后在 <script setup> 中声明一个同名的 ref 变量,Vue 会自动把子组件的暴露出来的内容挂载到这个 ref 上。
<!-- 子组件 ChildForm.vue -->
<template>
<form>
<input v-model="formData.name" />
<input v-model="formData.email" />
</form>
</template>
<script setup>
import { reactive } from 'vue'
const formData = reactive({ name: '', email: '' })
function resetForm() {
formData.name = ''
formData.email = ''
}
// 明确向父组件暴露 resetForm 方法
defineExpose({ resetForm })
</script>
<!-- 父组件 Parent.vue -->
<template>
<ChildForm ref="formRef" />
<button @click="handleReset">重置表单</button>
</template>
<script setup>
import { ref } from 'vue'
import ChildForm from './ChildForm.vue'
// 变量名必须与模板中的 ref 属性一致
const formRef = ref(null)
function handleReset() {
// 调用子组件暴露的方法
formRef.value.resetForm()
}
</script>
要点:
ref变量名必须与模板中声明的ref属性值完全一致,Vue 会在模板编译时自动完成绑定。- 访问子组件暴露的内容需要使用
.value(因为formRef本身是一个 ref 包裹的对象)。 - 默认情况下,
<script setup>组件内部的变量、方法都是私有的,父组件无法直接访问。
defineExpose:控制暴露范围
如果不使用 defineExpose,父组件通过 ref 只能拿到一个空对象。这是因为在 <script setup> 模式下,所有顶级绑定(数据、方法、计算属性)默认对外不可见。只有明确通过 defineExpose 导出的内容,才会出现在父组件拿到的 ref 实例上。
<script setup>
import { ref } from 'vue'
const internalState = ref('secret')
const sharedData = ref('public')
function doSomething() { ... }
function callableMethod() { ... }
defineExpose({
sharedData,
callableMethod
})
</script>
这样父组件可以访问 sharedData 和调用 callableMethod(),但无法触及 internalState 和 doSomething()。这是一种最小权限原则:只暴露父组件真正需要的东西,避免内部实现被意外依赖,提升封装性。
真实场景举例
- 表单重置/校验
父组件中包含多个步骤的表单,每个步骤是一个子组件。点击“提交”时,父组件调用子组件的 validate() 方法进行校验;点击“重置”时,调用子组件的 reset() 方法清空所有输入。
- 弹窗/抽屉控制
子组件是一个全局的确认弹窗,父组件通过 ref 调用它的 show()、hide()、openWithData(data) 方法,而不是在 Props 中传递一个复杂的 visible 属性并费力同步状态。
- 图表/地图实例调用
子组件封装了第三方可视化库,父组件可能需要调用图表实例的 updateChart() 或 exportImage() 方法,子组件通过 defineExpose 将图表实例的引用暴露出去。
注意事项与误区
- 不要过度使用
如果可以通过 Props 和 Events 实现通信,优先使用那种方式。ref 调用子组件方法会让父子耦合变紧,违背组件自闭环设计的初衷。只在“必须触发内部行为”时使用。
- 确保子组件已挂载
在 onMounted 之前,formRef.value 可能为 null。如果你在 setup 同步代码中直接调用子组件方法,很可能会报错。通常将调用放在事件回调里,或者在 onMounted 中安全访问。
- 避免直接修改子组件数据
通过 ref 暴露响应式数据给父组件,可以让父组件直接修改它。但这会破坏单向数据流,导致状态变更难以追踪。更好的做法是暴露方法,由子组件自己维护数据的修改。
- 与
$refs的差异(选项式 API)
在选项式 API 中,父组件通过 this.$refs.xxx 获取子组件实例,默认可以访问子组件的所有 data、methods。而 <script setup> + defineExpose 提供了更严格的封装,这是组合式 API 更安全的实践。
简单总结:当父子组件之间需要“父调子”时,给子组件加 ref,子组件用 defineExpose 开门,父组件就能调用指定的方法。这个机制简单直接,但要控制好暴露粒度,避免滥用。