第 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 | 跨层级 |
动手练习
- 计数器:父组件传
count,子组件按钮@clickemitupdate - v-model 输入框:用
defineModel(Vue 3.4 简化v-model)封装一个 input - 命令式弹窗:写
Modal.vue,父组件调用openModal('标题')打开
下一章:第 7 章:生命周期 →