Skip to content
第 6 章 前端 ⏱ 13 分钟阅读

第 6 章:组件通信 ​

学习目标 ​

  • 掌握 4 种基本通信方式
  • 区分 defineEmits 与 defineProps
  • 用 v-model 实现父子双向绑定
  • 选出当前场景最合适的方案

一、Props / Emits(最常用) ​

vue
<!-- 子组件 -->
<script setup lang="ts">
const props = defineProps<{ count: number }>()
const emit = defineEmits<{
  (e: 'update', val: number): void
}>()
</script>

<template>
  <button @click="emit('update', props.count + 1)">+1</button>
</template>
vue
<!-- 父组件 -->
<script setup lang="ts">
import Counter from './Counter.vue'
import { ref } from 'vue'

const n = ref(0)
</script>

<template>
  <Counter :count="n" @update="n = $event" />
</template>

⚠️ 坑 1:emit 事件名建议用 kebab-case(@my-event),TS 类型用 camelCase。

二、v-model(双向绑定语法糖) ​

vue
<!-- 子组件 -->
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ (e: 'update:modelValue', v: string): void }>()
</script>

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
  />
</template>
vue
<!-- 父组件 -->
<MyInput v-model="username" />

可以绑定多个 v-model:

vue
<!-- 子组件 -->
defineProps<{ title: string; content: string }>()
defineEmits<{ (e: 'update:title', v: string): void; (e: 'update:content', v: string): void }>()

<!-- 父组件 -->
<Editor v-model:title="t" v-model:content="c" />

三、defineExpose 暴露给父组件 ​

vue
<!-- 子组件 -->
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
const reset = () => (count.value = 0)

defineExpose({ count, reset })   // 父组件 ref 才能拿到
</script>
vue
<!-- 父组件 -->
<script setup lang="ts">
import { ref } from 'vue'
import Counter from './Counter.vue'

const counterRef = ref<InstanceType<typeof Counter> | null>(null)
counterRef.value?.reset()
</script>

<template>
  <Counter ref="counterRef" />
</template>

⚠️ 坑 2:<script setup> 默认是私有的,父组件拿不到任何东西,必须 defineExpose。

四、ref / 模板引用 ​

vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const inputRef = ref<HTMLInputElement | null>(null)

onMounted(() => inputRef.value?.focus())
</script>

<template>
  <input ref="inputRef" />
</template>

普通 DOM 元素拿到的就是原生 HTMLElement。

五、provide / inject(跨层级) ​

vue
<!-- 祖先 -->
<script setup lang="ts">
import { provide, ref } from 'vue'
const theme = ref('light')
provide('theme', theme)
</script>
vue
<!-- 后代 -->
<script setup lang="ts">
import { inject } from 'vue'
const theme = inject<Ref<string>>('theme', ref('light'))
</script>

💡 inject 第二个参数是默认值:找不到对应 provide 时用它兜底;不写则返回 undefined,模板渲染空 + .value 可能炸。

三种写法对照:

ts
// ① 没默认值:找不到时返回 undefined(可能炸)
const theme = inject<Ref<string>>('theme')   // 类型是 Ref<string> | undefined

// ② 直接给默认值:基本类型够用
const theme = inject<Ref<string>>('theme', ref('light'))   // 类型是 Ref<string>

// ③ 工厂函数(官方推荐,Vue 3.3+):对象/数组用它防多个组件串值
const theme = inject<Ref<string>>('theme', () => ref('light'))

为啥要用"工厂"写法:

ts
// ❌ 写法 ② 有坑:多个组件 inject 时,共用同一个 ref 实例,会互相干扰
const defaultTheme = ref('light')
CompA 用 inject('theme', defaultTheme)
CompB 用 inject('theme', defaultTheme)   // ← 跟 CompA 同一个 ref
CompA.theme.value = 'dark' → CompB 也跟着变 ❌

// ✅ 写法 ③ 每次调用工厂 → 各自一个新 ref,互不干扰
CompA 用 inject('theme', () => ref('light'))   // 新 ref
CompB 用 inject('theme', () => ref('light'))   // 又一个新 ref ✓

选型:基本类型(字符串/数字)用 ②;对象/数组/嵌套 ref 用 ③ 防串。

⚠️ 坑 3:provide 传响应式数据必须传 ref,否则后代改了不更新。

六、选型速查 ​

场景推荐
父 → 子props
子 → 父emit
双向v-model
命令式调用ref + defineExpose
跨多层provide / inject
通用状态Pinia(第 11 章)

七、本章小结 ​

方式方向
props父 → 子
emit子 → 父
v-model双向
ref + defineExpose父命令式调子
provide / inject跨层级

动手练习 ​

  1. 计数器:父组件传 count,子组件按钮 @click emit update
  2. v-model 输入框:用 defineModel(Vue 3.4 简化 v-model)封装一个 input
  3. 命令式弹窗:写 Modal.vue,父组件调用 openModal('标题') 打开

下一章:第 7 章:生命周期 →

本站基于 VitePress 构建 · 由 StackHub 团队维护