Appearance
Vue 3 父子组件通信(只讲父子)
父子组件通信最常用、最推荐的两条主线:
- 父传子:Props(单向数据流)
- 子传父:Emits(事件上抛)
在此基础上,Vue 3 还提供了更“语法糖化”的 v-model(本质还是 Props + Emits),以及“父直接调用子方法”的 ref + defineExpose(更偏命令式,慎用)。
父传子:Props
父组件通过在子组件标签上绑定属性,把数据传给子组件。
子组件(TypeScript 定义 Props)
vue
<script setup lang="ts">
type Props = {
title: string
count?: number
}
const props = withDefaults(defineProps<Props>(), {
count: 0
})
</script>
<template>
<h3>{{ props.title }}</h3>
<p>count: {{ props.count }}</p>
</template>父组件(向子组件传值)
vue
<script setup lang="ts">
import ChildCard from './ChildCard.vue'
const title = '文章列表'
const count = 3
</script>
<template>
<ChildCard :title="title" :count="count" />
</template>注意点
- Props 是单向:父更新会下发到子;子不应该直接修改 props
- 子想基于 props 做可修改的本地状态,用
ref/reactive创建副本即可
子传父:Emits
子组件通过触发事件,把信息“上抛”给父组件,父组件监听并处理。
子组件(定义并触发事件)
vue
<script setup lang="ts">
const emit = defineEmits<{
(e: 'add', delta: number): void
(e: 'reset'): void
}>()
function handleAdd() {
emit('add', 1)
}
function handleReset() {
emit('reset')
}
</script>
<template>
<button type="button" @click="handleAdd">+1</button>
<button type="button" @click="handleReset">reset</button>
</template>父组件(监听事件并更新状态)
vue
<script setup lang="ts">
import CounterButtons from './CounterButtons.vue'
import { ref } from 'vue'
const count = ref(0)
function onAdd(delta: number) {
count.value += delta
}
function onReset() {
count.value = 0
}
</script>
<template>
<p>count: {{ count }}</p>
<CounterButtons @add="onAdd" @reset="onReset" />
</template>v-model:双向绑定(本质是 Props + Emits)
当你需要“父把值传给子,子修改后再同步回父”,推荐使用 v-model(而不是让子直接改 props)。
子组件(modelValue + update:modelValue)
vue
<script setup lang="ts">
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()
function onInput(e: Event) {
const value = (e.target as HTMLInputElement).value
emit('update:modelValue', value)
}
</script>
<template>
<input :value="props.modelValue" @input="onInput" />
</template>父组件(使用 v-model)
vue
<script setup lang="ts">
import TextInput from './TextInput.vue'
import { ref } from 'vue'
const keyword = ref('')
</script>
<template>
<TextInput v-model="keyword" />
<p>keyword: {{ keyword }}</p>
</template>父调用子:ref + defineExpose(命令式,谨慎使用)
有些场景(例如:父组件需要触发子组件内部的 focus()、重置表单等)可以用 ref 直接拿到子组件暴露的方法。
子组件(显式暴露能力)
vue
<script setup lang="ts">
import { ref } from 'vue'
const inputRef = ref<HTMLInputElement | null>(null)
function focus() {
inputRef.value?.focus()
}
function clear() {
if (inputRef.value) inputRef.value.value = ''
}
defineExpose({
focus,
clear
})
</script>
<template>
<input ref="inputRef" />
</template>父组件(通过 ref 调用)
vue
<script setup lang="ts">
import FocusableInput from './FocusableInput.vue'
import { ref } from 'vue'
type Exposed = {
focus: () => void
clear: () => void
}
const childRef = ref<Exposed | null>(null)
</script>
<template>
<FocusableInput ref="childRef" />
<button type="button" @click="childRef?.focus()">focus</button>
<button type="button" @click="childRef?.clear()">clear</button>
</template>什么时候用它
- 只在“必须命令式”的场景使用(焦点控制、滚动定位、调用第三方库实例等)
- 业务数据流仍然优先 Props/Emits/v-model,避免逻辑分散难维护
最佳实践总结
- 父传子:优先 Props
- 子传父:优先 Emits
- 需要双向:优先 v-model(仍然是 Props + Emits)
- 必须命令式:ref + defineExpose(谨慎使用)