第 1 节 概述
上一篇文章我们把页面拆成了多个组件。
组件是独立的,但在实际应用中,它们之间需要传递数据。
比如:父组件获取了用户信息,子组件(用户卡片)需要展示它。用户点了子组件的按钮,父组件需要知道。
Vue 3 用两种方式解决:
- props:父组件把数据传给子组件
- 事件:子组件通知父组件"发生了什么"
第 2 节 props:父传子
2.1 基本用法
父组件通过属性传值,子组件用 defineProps 声明接收:
1
2
3
4
5
6
7
8
9
10
| <!-- Parent.vue -->
<script setup lang="ts">
import Child from '@/components/Child.vue'
const title = ref('Vue 3 入门教程')
</script>
<template>
<Child :title="title" />
</template>
|
1
2
3
4
5
6
7
8
9
10
11
12
| <!-- Child.vue -->
<script setup lang="ts">
const props = defineProps(['title'])
// 在 script 中用 props.title 读取
console.log(props.title)
</script>
<template>
<h1>{{ title }}</h1>
<!-- 模板中可以直接用 title -->
</template>
|
说明
defineProps 是 Vue 3 的一个编译宏,不需要从 vue 中导入。它在编译阶段就会被处理掉,不会出现在最终代码中。
2.2 传递各种类型
| 类型 | 父组件写法 | 子组件接收 |
|---|
| 字符串 | <Child title="你好" /> | defineProps(['title']) |
| 数字 | <Child :count="42" /> | defineProps(['count']) |
| 布尔 | <Child :published="true" /> | defineProps(['published']) |
| 数组 | <Child :list="[1, 2, 3]" /> | defineProps(['list']) |
| 对象 | <Child :user="{ name: '张三' }" /> | defineProps(['user']) |
注意两点:
- 不加
: 的是字符串,加 : 的是变量或表达式 - 布尔属性的特殊规则:只写属性名没有值,等同于
:prop="true",不写属性则等同于 :prop="false"
1
2
3
4
5
| <template>
<!-- 以下两种写法等效 -->
<Child disabled />
<Child :disabled="true" />
</template>
|
2.3 用对象绑定多个属性
如果一个对象中所有属性都要传给子组件,可以用 v-bind 绑定整个对象:
1
| const post = { id: 1, title: 'Vue 入门', author: '张三' }
|
1
2
3
4
5
| <template>
<BlogPost v-bind="post" />
<!-- 等同于 -->
<BlogPost :id="post.id" :title="post.title" :author="post.author" />
</template>
|
2.4 单向数据流
props 是只读的,子组件不能修改它。
1
2
3
| // ❌ 不要这样做
const props = defineProps(['title'])
props.title = '新标题' // 会报错
|
这是 Vue 的一条铁律:数据只能从父组件流向子组件,不能反向修改。
那如果子组件需要基于 props 做操作怎么办?
场景一:只用 props 做初始值
子组件需要把 props 的值作为初始值,之后自己独立维护。
正确的做法是复制到 ref:
1
2
3
4
5
6
7
8
| <script setup lang="ts">
import { ref } from 'vue'
const props = defineProps(['initialCount'])
// ✅ 复制到本地 ref
const count = ref(props.initialCount)
</script>
|
场景二:需要对 props 做转换
比如把传入的字符串转成大写再展示:
1
2
3
4
5
6
7
8
| <script setup lang="ts">
import { computed } from 'vue'
const props = defineProps(['message'])
// ✅ 用 computed 派生新值
const upperMessage = computed(() => props.message.toUpperCase())
</script>
|
2.5 类型校验
defineProps 可以用对象写法来声明类型和规则:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| <script setup lang="ts">
defineProps({
// 基本类型检查
title: String,
likes: Number,
// 多个候选类型
id: [String, Number],
// 必填属性
username: {
type: String,
required: true,
},
// 有默认值
count: {
type: Number,
default: 0,
},
// 对象类型需要工厂函数
user: {
type: Object,
default: () => ({ name: '默认用户' }),
},
// 自定义校验
status: {
validator: (value: string) => {
return ['success', 'warning', 'danger'].includes(value)
},
},
})
</script>
|
| 校验选项 | 说明 |
|---|
type | 类型构造函数,如 String、Number、Boolean |
required | 是否必填 |
default | 默认值(对象和数组必须用函数返回) |
validator | 自定义校验函数,返回 false 时控制台会警告 |
2.6 TypeScript 写法
用 TypeScript 泛型可以让类型更清晰:
1
2
3
4
5
6
7
| <script setup lang="ts">
defineProps<{
title: string
likes?: number // 可选属性
author: string
}>()
</script>
|
设置默认值需要用 withDefaults:
1
2
3
4
5
6
7
8
9
10
| <script setup lang="ts">
interface Props {
title: string
likes?: number
}
const props = withDefaults(defineProps<Props>(), {
likes: 0,
})
</script>
|
第 3 节 事件:子传父
3.1 基本用法
子组件用 $emit 发送事件,父组件用 @ 监听:
1
2
3
4
| <!-- Child.vue -->
<template>
<button @click="$emit('clicked')">点击</button>
</template>
|
1
2
3
4
5
6
7
8
9
10
| <!-- Parent.vue -->
<template>
<Child @clicked="handleClick" />
</template>
<script setup lang="ts">
function handleClick() {
console.log('子组件被点击了')
}
</script>
|
3.2 声明事件
用 defineEmits 声明子组件会触发哪些事件:
1
2
3
4
5
6
7
8
| <!-- Child.vue -->
<script setup lang="ts">
const emit = defineEmits(['submit', 'cancel'])
function onSubmit() {
emit('submit')
}
</script>
|
声明事件的好处:
- 文档化:阅读代码时能清楚看到子组件支持哪些事件
- 避免冲突:声明过的事件不会作为"透传属性"落到根元素
3.3 事件参数
子组件可以带参数给父组件:
1
2
3
4
| <!-- Child.vue -->
<template>
<button @click="$emit('increaseBy', 1)">+1</button>
</template>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| <!-- Parent.vue -->
<template>
<Child @increase-by="(n) => count += n" />
<!-- 或者用方法 -->
<Child @increase-by="increaseCount" />
</template>
<script setup lang="ts">
const count = ref(0)
function increaseCount(n: number) {
count.value += n
}
</script>
|
3.4 TypeScript 写法
1
2
3
4
5
6
7
8
| <script setup lang="ts">
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
emit('change', 1)
</script>
|
3.5 事件校验
声明事件时还可以做校验:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| <script setup lang="ts">
const emit = defineEmits({
// 不校验
click: null,
// 校验参数
submit: ({ email, password }: { email: string; password: string }) => {
if (email && password) {
return true
}
console.warn('submit 事件参数不完整')
return false
},
})
</script>
|
校验不通过只会发警告,不会阻止事件发送。
第 4 节 父子通信总结
1
2
| 父组件 ──props 传数据──→ 子组件
父组件 ←──emit 发事件─── 子组件
|
| 方式 | 方向 | 用途 |
|---|
| props | 父 → 子 | 传递数据、配置项 |
| $emit | 子 → 父 | 通知父组件发生了某个动作 |
这两种是最基础的组件通信方式。它们已经可以解决大部分场景。
但有一个情况比较尴尬:如果组件层级很深(爷→父→子→孙),要通过 props 一层层传递,代码会变得很啰嗦。
在解决这个问题之前,还有一个场景要先处理:父组件想往子组件里传一段 HTML 模板,而不是传数据。这该怎么做?