2282 字
11 分钟
Vue 3 父子组件通信:props、事件与实例暴露

通信的三个方向#

Vue 的数据流是单向的:父组件通过 props 把数据传下去,子组件通过事件把变化报上来。子组件不能直接改父组件的状态。

需要父组件主动调用子组件内部方法时,用 defineExpose 显式暴露。这是第三条路,动手之前先想清楚有没有更合适的做法。

组件上的 v-model 是 props 加事件拼出来的语法糖。

静态传值和动态绑定#

不加 : 的属性按 HTML 原生属性处理,值默认是字符串,只能传固定文本。

Parent.vue
<Child msg="我是字符串" />

需要传数字、布尔、数组、对象或变量时,加 : 或 v-bind。Vue 把它当 JavaScript 表达式求值,值保持原本的类型。

Parent.vue
<Child
:num="123"
:isShow="true"
:list="[1, 2, 3]"
:user="{ name: 'zs' }"
:custom="data"
/>

这五行分别是数字、布尔、数组、对象和变量,类型都保持原样。

运行时的 props 声明#

defineProps 和 defineEmits 是 <script setup> 的编译器宏,不需要 import。运行时声明用对象形式,配置项和组件的 props 选项完全一样。

Child.vue
<script setup>
const props = defineProps({
msg: String,
title: {
type: String,
required: true
},
num: {
type: Number,
default: 0,
validator: (val) => val > 0
}
})
</script>
配置作用
typeprop 的数据类型
required是否必须传入
default父组件没传时使用的值
validator自定义校验函数,返回 false 时控制台告警

type 可以取 String、Number、Boolean、Array、Object、Date、Function、Symbol、Error。也可以直接写自定义类,这时 Vue 用 instanceof 检查传入的值。

类型声明#

TS 项目可以只写类型,不写 type、required、default 这些运行时字段,把校验交给 TypeScript。

Child.vue
<script setup lang="ts">
const props = defineProps<{
msg: string
num?: number
list: number[]
user: { name: string }
fn: (id: number) => void
}>()
</script>

必传的属性直接写类型,可选的属性加 ?。开发环境下编译器会从类型反推出等价的运行时声明,所以运行时校验照样有。

属性多了以后,抽到 interface 里更清楚。

Child.vue
<script setup lang="ts">
interface User {
name: string
age: number
}
defineProps<{
user: User
list?: number[]
}>()
</script>

两种声明方式的取舍#

对比项运行时声明类型声明
适用JS 和 TS 都能用需要 TypeScript
校验时机运行时,控制台告警编译期,编辑器直接标红
类型提示无props.xxx 有完整提示
默认值直接写 default3.5 及以上用解构默认值,3.4 及以下用 withDefaults

两种声明不能同时用,同时写会编译报错。

props 的默认值#

Vue 3.5 起,类型声明可以直接用 JavaScript 的解构默认值,不需要额外的宏。

Child.vue
<script setup lang="ts">
interface Props {
msg?: string
labels?: string[]
}
const { msg = 'hello', labels = ['one', 'two'] } = defineProps<Props>()
</script>

withDefaults:3.4 及以下的写法#

withDefaults 的第一个参数是 defineProps 的结果,第二个参数是默认值对象。

Child.vue
<script setup lang="ts">
interface Props {
msg?: string
num?: number
list?: number[]
}
const props = withDefaults(defineProps<Props>(), {
msg: '默认消息',
num: 100,
list: () => [1, 2, 3]
})
</script>

它生成等价的运行时 default 选项,同时把返回的 props 类型里已经给了默认值的属性去掉 ?。上面这段代码里,props.num 的类型是 number,不再是 number | undefined。

参考 withDefaults。

引用类型的默认值#

数组和对象的默认值必须包在函数里返回。每次求值都会新建一份,每个组件实例拿到独立的数据。

defineProps({
list: {
type: Array,
default: () => []
},
user: {
type: Object,
default: () => ({})
}
})

直接写 list: [1, 2, 3] 会让所有实例共用同一个数组,改一个会影响全部。用 withDefaults 时同理,工厂函数写在第二个参数里。

声明自定义事件#

子组件不能直接改父组件的数据。父组件想知道子组件里发生了什么,只能由子组件主动派发一个事件。

defineEmits 用来声明当前组件会派发哪些事件。声明之后,父组件用 @事件名 监听,回调函数收到子组件传出来的参数。

数组写法#

数组写法只声明事件名,不约束参数格式,JS 和 TS 项目都能用。

Child.vue
<script setup>
const emit = defineEmits(['sendData', 'changeCount'])
const handleClick = () => {
emit('sendData', { name: '小明', age: 18 })
emit('changeCount', 100)
}
</script>
<template>
<button @click="handleClick">向父组件传值</button>
</template>
Parent.vue
<script setup>
const getChildData = (data) => {
console.log(data) // { name: '小明', age: 18 }
}
const handleCount = (num) => {
console.log(num) // 100
}
</script>
<template>
<Child @sendData="getChildData" @changeCount="handleCount" />
</template>

对象写法#

对象写法可以在声明事件的同时加上参数校验。

Child.vue
<script setup>
const emit = defineEmits({
// 不校验
sendData: null,
// 校验 submit 的参数
submit: ({ email, password }) => {
if (email && password) {
return true
}
console.warn('submit 的参数不合法')
return false
}
})
</script>

校验函数返回 false 时,控制台会打印告警。

类型声明写法#

类型声明写法约束事件名、参数类型和返回值,参数写错编辑器直接标红。

Child.vue
<script setup lang="ts">
// 格式:(事件名:字面量,参数:类型):返回值
const emit = defineEmits<{
(e: 'sendData', user: { name: string; age: number }): void
(e: 'changeCount', count: number): void
(e: 'close'): void
}>()
const handleClick = () => {
emit('sendData', { name: '小红', age: 20 })
emit('changeCount', 66)
emit('close')
}
</script>

事件多的时候可以把参数类型抽出去。Vue 3.3 起还支持具名元组写法,看起来更像一个对象的字段。

Child.vue
<script setup lang="ts">
type User = {
name: string
age: number
}
type Emits = {
sendData: [user: User]
changeCount: [count: number]
close: []
}
const emit = defineEmits<Emits>()
</script>

事件名和参数#

一次可以派发多个参数,父组件的回调按顺序接收。

// 子组件:一次派发多个参数
emit('info', '标题', 200, [1, 2, 3])
// 父组件:回调按顺序接收
const getInfo = (title, num, list) => {
console.log(title, num, list)
}

事件名用小驼峰或短横线都行,模板里两种写法可以互相匹配,团队内保持一致即可。

props 是只读的,子组件不能给它重新赋值,控制台会告警。要让父组件改数据,就派发一个事件出去,由父组件自己决定怎么改。

// 不要这样改
props.name = 'xxx'
// 应该这样通知父组件
emit('update:name', 'xxx')

未声明的事件会落到透传属性#

组件自定义事件不会冒泡,只有直接父组件能监听到。

父组件写的 @close 如果没在子组件的 emits 里声明,Vue 只把它当普通 attribute,落到子组件的根元素上,等价于在根元素上挂了一个原生 DOM 监听器。

组件上的 v-model#

组件上的 v-model 是 props 加事件拼出来的语法糖:父组件传一个 modelValue,子组件在需要更新时派发 update:modelValue。

Child.vue
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
Parent.vue
<script setup>
import { ref } from 'vue'
const text = ref('')
</script>
<template>
<Child v-model="text" />
</template>

用 v-model:name 可以换 prop 的名字,一个组件上也能同时绑多个。父组件写 v-model:first-name,子组件对应的是 firstName prop 和 update:firstName 事件。

Parent.vue
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>

defineModel 从 3.4 起稳定可用(3.3 有实验版本),上面那套 props 加事件可以合并成一行。

Child.vue
<script setup>
const model = defineModel()
model.value = 'hello'
</script>

defineExpose 暴露子组件内部#

<script setup> 写法的组件默认关闭:父组件通过模板引用拿到的实例上,看不到组件内部声明的变量、方法和状态。

要在父组件里用,子组件必须用 defineExpose 显式写出暴露哪些内容。没写进去的一律访问不到。

Child.vue
<script setup>
import { ref } from 'vue'
const childMsg = ref('我是子组件的消息')
const childMethod = () => {
console.log('我是子组件方法')
}
defineExpose({
childMsg,
childMethod
})
</script>

父组件用一个同名的 ref 接收实例。ref 在挂载完成之后才会绑上,所以访问要放在 onMounted 里,否则拿到的是 null。

Parent.vue
<script setup>
import { onMounted, ref } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
onMounted(() => {
console.log(childRef.value.childMsg)
childRef.value.childMethod()
})
</script>
<template>
<Child ref="childRef" />
</template>

暴露出去的 ref 会保持响应式:子组件里改 childMsg,父组件拿到的值跟着更新。

对比一下 Options API:data 和 methods 都挂在实例上,父组件直接就能拿到。<script setup> 把默认行为反过来,只有显式暴露的才可见。

父组件里 childRef 的类型是 any,拿不到暴露成员的提示。用 TS 时改成下面这样。

Parent.vue
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import Child from './Child.vue'
const childRef = ref<InstanceType<typeof Child> | null>(null)
onMounted(() => {
console.log(childRef.value?.childMsg)
childRef.value?.childMethod()
})
</script>

参考 defineExpose。

第 13 篇,共 19 篇本文属于系列

Vue 3 父子组件通信:props、事件与实例暴露
https://www.choria.top/posts/vue3-prop-emit/
作者
乔瑞雅·克林威尔
发布于
2026-04-13
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取

最后更新于 ,距今已过 209 天

部分内容可能已过时