Skip to content

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(谨慎使用)