文章目录
- 前言
- 一、Props 基础
-
- 1.1 定义
- 1.2 声明方式
- 1.3 支持的类型
- 1.4 TypeScript 写法
- 二、Emit 基础
-
- 2.1 定义
- 2.2 声明方式
- 2.3 事件修饰符
- 三、单向数据流
-
- 3.1 核心原则
- 3.2 常见模式
- 四、解构 Props 与响应性
-
- 4.1 问题
- 4.2 解决方案
- 五、应用场景
-
- 5.1 列表组件
- 5.2 表单组件
- 5.3 通用按钮
- 5.4 分页组件
- 六、编译器宏
-
- 6.1 defineProps / defineEmits 特性
- 七、面试聚焦
-
- 7.1 解构 Props 失去响应性
- 7.2 defineProps 需要导入吗?
- 7.3 Props 类型校验
- 八、易混淆点
- 九、思考与练习
- 总结
前言
Props 和 Emit 是 Vue 最基础的父子通信方式,也是组件接口设计的核心。本篇会讲清楚:
- defineProps 的类型校验与默认值
- defineEmits 的事件声明与触发
- 单向数据流原则
- v-model 与 Props/Emit 的关系
- 解构 Props 失去响应性的解决方案
一、Props 基础
1.1 定义
Props 是父组件向子组件传递数据的单向数据通道,子组件通过 defineProps 声明期望接收的属性列表。
<!– 父组件 –>
<script setup>
import Child from './Child.vue'
const title = ref('Hello')
const count = ref(0)
</script>
<template>
<Child :title="title" :count="count" />
</template>
<!– 子组件 Child.vue –>
<script setup>
const props = defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 }
})
</script>
<template>
<h1>{{ title }}: {{ count }}</h1>
</template>
1.2 声明方式
// 1. 数组语法(无类型校验)
defineProps(['title', 'count'])
// 2. 对象语法(推荐,支持校验)
defineProps({
title: String,
count: Number,
disabled: Boolean
})
// 3. 完整对象语法
defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
},
items: {
type: Array,
default: () => [] // 对象/数组默认值必须用工厂函数
},
config: {
type: Object,
default: () => ({})
}
})
1.3 支持的类型
defineProps({
// 基础类型
str: String,
num: Number,
bool: Boolean,
// 复合类型
arr: Array,
obj: Object,
fn: Function,
// 多种可能类型
id: [String, Number],
// 自定义校验
age: {
type: Number,
validator: (value) => value >= 0 && value <= 150
}
})
1.4 TypeScript 写法
<script setup lang="ts">
// 类型声明
interface Props {
title: string
count?: number
items?: string[]
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => []
})
</script>
二、Emit 基础
2.1 定义
Emit 是子组件向父组件通信的事件机制,子组件通过 defineEmits 声明可触发的事件列表。
<!– 子组件 –>
<script setup>
const emit = defineEmits(['change', 'submit'])
const handleClick = () => {
emit('change', 42)
}
const handleSubmit = () => {
emit('submit', { name: 'Alice', age: 25 })
}
</script>
<template>
<button @click="handleClick">触发 change</button>
<button @click="handleSubmit">提交</button>
</template>
<!– 父组件 –>
<script setup>
const onChange = (val) => console.log('收到:', val)
const onSubmit = (data) => console.log('提交:', data)
</script>
<template>
<Child @change="onChange" @submit="onSubmit" />
</template>
2.2 声明方式
// 1. 数组语法
defineEmits(['change', 'submit'])
// 2. 对象语法(带校验)
defineEmits({
change: (val) => typeof val === 'number',
submit: (data) => data && typeof data.name === 'string'
})
// 3. TypeScript
const emit = defineEmits<{
change: [value: number]
submit: [data: { name: string; age: number }]
}>()
2.3 事件修饰符
<!– 父组件监听子组件事件 –>
<Child @change.once="handleChange" /> <!– 只触发一次 –>
<Child @submit.prevent="handleSubmit" /> <!– 阻止默认行为 –>
三、单向数据流
3.1 核心原则
// 数据流向:父 → 子(Props)
// 事件流向:子 → 父(Emit)
// ❌ 错误:子组件直接修改 props
const props = defineProps({ count: Number })
props.count++ // 开发模式会警告
// ✅ 正确:通过 emit 通知父组件修改
const emit = defineEmits(['update:count'])
const increment = () => emit('update:count', props.count + 1)
3.2 常见模式
<!– 模式 1:子组件通知父组件更新 –>
<script setup>
const props = defineProps({ value: String })
const emit = defineEmits(['update:value'])
const onInput = (e) => {
emit('update:value', e.target.value)
}
</script>
<template>
<input :value="value" @input="onInput" />
</template>
<!– 模式 2:v-model 语法糖(Vue 3) –>
<!– 子组件 –>
<script setup>
const props = defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])
const onInput = (e) => {
emit('update:modelValue', e.target.value)
}
</script>
<!– 父组件 –>
<MyInput v-model="text" />
<!– 等价于 :modelValue="text" @update:modelValue="text = $event" –>
<!– 模式 3:多个 v-model –>
<script setup>
defineProps({
name: String,
age: Number
})
defineEmits(['update:name', 'update:age'])
</script>
<!– 父组件 –>
<MyForm v-model:name="name" v-model:age="age" />
四、解构 Props 与响应性
4.1 问题
const props = defineProps({ count: Number })
// ❌ 解构后失去响应性
const { count } = props
count // 不会随 props.count 更新
// ❌ 直接解构 defineProps 返回值
const { count } = defineProps({ count: Number })
4.2 解决方案
import { toRefs, toRef } from 'vue'
const props = defineProps({ count: Number, name: String })
// ✅ 方案 1:toRefs 解构所有 props
const { count, name } = toRefs(props)
count.value // 保持响应性
// ✅ 方案 2:toRef 解构单个 prop
const count = toRef(props, 'count')
// ✅ 方案 3:模板中直接使用 props.xxx(无需解构)
// <template>{{ props.count }}</template>
// ✅ 方案 4:computed 派生
const double = computed(() => props.count * 2)
五、应用场景
5.1 列表组件
<!– List.vue –>
<script setup>
defineProps({
items: { type: Array, required: true },
loading: { type: Boolean, default: false }
})
</script>
<template>
<div v-if="loading">加载中…</div>
<ul v-else>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</template>
5.2 表单组件
<!– Form.vue –>
<script setup>
const props = defineProps({
modelValue: { type: Object, required: true }
})
const emit = defineEmits(['update:modelValue', 'submit'])
const updateField = (key, value) => {
emit('update:modelValue', { …props.modelValue, [key]: value })
}
const handleSubmit = () => {
emit('submit', props.modelValue)
}
</script>
5.3 通用按钮
<!– Button.vue –>
<script setup>
defineProps({
type: {
type: String,
default: 'primary',
validator: (v) => ['primary', 'danger', 'default'].includes(v)
},
disabled: { type: Boolean, default: false }
})
defineEmits(['click'])
</script>
<template>
<button
:class="type"
:disabled="disabled"
@click="$emit('click', $event)"
>
<slot />
</button>
</template>
5.4 分页组件
<!– Pagination.vue –>
<script setup>
defineProps({
page: { type: Number, default: 1 },
pageSize: { type: Number, default: 10 },
total: { type: Number, required: true }
})
const emit = defineEmits(['page-change'])
const changePage = (newPage) => {
emit('page-change', newPage)
}
</script>
六、编译器宏
6.1 defineProps / defineEmits 特性
// 1. 编译器宏,无需 import
// ❌ import { defineProps } from 'vue'
// 2. 只能在 <script setup> 顶层使用
// ❌ 不能在条件语句、循环中使用
if (condition) {
defineProps({ … }) // 错误
}
// 3. 返回值是只读的响应式对象
const props = defineProps({ count: Number })
// props.count = 1 // 警告,不应修改
// 4. 可在模板中直接使用 prop 名(无需 props. 前缀)
// defineProps({ title: String })
// <template>{{ title }}</template>
七、面试聚焦
7.1 解构 Props 失去响应性
// 问题:解构后 count 是普通值,不会更新
const { count } = defineProps({ count: Number })
// 解决:toRefs
const props = defineProps({ count: Number })
const { count } = toRefs(props)
7.2 defineProps 需要导入吗?
// 不需要。defineProps 和 defineEmits 是编译器宏
// 编译时会被处理,不是运行时函数
7.3 Props 类型校验
// 数组语法无法校验
defineProps(['count']) // 任何类型都能传入
// 对象语法可以校验
defineProps({
count: { type: Number, required: true }
}) // 传入字符串会警告
八、易混淆点
九、思考与练习
1. 为什么子组件不能直接修改 props?
解析:Vue 遵循单向数据流,props 由父组件拥有和控制。子组件修改会破坏数据流的可预测性,应通过 emit 通知父组件修改。
2. 解构 props 为什么会失去响应性?如何解决?
解析:解构得到的是普通值快照。使用 toRefs(props) 或 toRef(props, 'key') 保持响应性。
3. defineProps 和 defineEmits 需要导入吗?
解析:不需要。它们是编译器宏,在 <script setup> 中直接使用,编译时处理。
4. v-model 在 Vue 3 中是如何工作的?
解析:
<MyInput v-model="text" />
<!– 等价于 –>
<MyInput :modelValue="text" @update:modelValue="text = $event" />
5. 对象类型的 default 为什么必须用工厂函数?
解析:如果直接写 default: {},所有组件实例会共享同一个对象引用,修改一个会影响其他实例。工厂函数每次创建新对象。
总结
- Props:父 → 子,单向数据通道,defineProps 声明
- Emit:子 → 父,事件机制,defineEmits 声明
- 单向数据流:子不修改 props,通过 emit 通知父
- v-model:Props + Emit 的语法糖
- 解构响应性:用 toRefs / toRef 保持响应性
- 编译器宏:defineProps / defineEmits 无需 import


