Vue 3 父子组件参数传递完整指南
Vue 3 中父子组件之间的参数传递有多种方式,我将详细介绍每种方法并提供实际示例。
📊 参数传递方式总览
| 方式 | 方向 | 使用场景 | 特点 |
|---|---|---|---|
| Props | 父 → 子 | 传递数据、配置 | 单向数据流,响应式 |
| 自定义事件 | 子 → 父 | 子组件通知父组件 | 通过 $emit 触发 |
| v-model | 双向 | 表单控件、组件 | 语法糖,双向绑定 |
| 插槽 | 父 → 子 | 内容分发 | 传递模板片段 |
| provide/inject | 祖先 → 后代 | 跨层级传递 | 避免逐层传递 |
| ref | 父 → 子 | 访问子组件实例 | 直接操作子组件 |
一、Props(父传子)
基本用法
父组件传递数据:
vue
<!-- ParentComponent.vue -->
<template>
<div>
<!-- 传递静态值 -->
<ChildComponent title="用户信息" />
<!-- 传递动态值 -->
<ChildComponent
:user="userData"
:count="counter"
:is-active="true"
/>
<!-- 传递数组和对象 -->
<ChildComponent
:items="itemsList"
:config="{ theme: 'dark', size: 'large' }"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const userData = ref({
name: '张三',
age: 25
})
const counter = ref(0)
const itemsList = ref(['项目1', '项目2', '项目3'])
</script>
子组件接收数据:
vue
<!-- ChildComponent.vue -->
<template>
<div>
<h2>{{ title }}</h2>
<p>用户: {{ user.name }} - {{ user.age }}岁</p>
<p>计数: {{ count }}</p>
<p>状态: {{ isActive ? '激活' : '未激活' }}</p>
<ul>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</div>
</template>
<script setup>
// defineProps 接收 props
const props = defineProps({
// 必填属性
user: {
type: Object,
required: true
},
// 可选属性
title: {
type: String,
default: '默认标题'
},
count: {
type: Number,
default: 0
},
isActive: Boolean,
items: Array
})
// 在 JS 中使用 props
console.log('接收到的用户数据:', props.user)
console.log('标题:', props.title)
</script>
TypeScript 支持
vue
<!-- ChildComponent.vue -->
<script setup lang="ts">
interface User {
name: string
age: number
email?: string
}
// TypeScript 写法
defineProps<{
user: User
title?: string
count: number
isActive?: boolean
items?: string[]
}>()
// 或使用 withDefaults
withDefaults(defineProps<{
title?: string
count?: number
}>(), {
title: '默认标题',
count: 0
})
</script>
二、自定义事件(子传父)
基本用法
子组件触发事件:
vue
<!-- ChildComponent.vue -->
<template>
<div>
<h3>子组件</h3>
<button @click="increment">增加计数</button>
<button @click="reset">重置</button>
<button @click="sendData">发送数据</button>
</div>
</template>
<script setup>
// defineEmits 定义事件
const emit = defineEmits([
'increment', // 简单事件
'update:count', // 用于 v-model
'submit' // 复杂事件
])
const increment = () => {
// 触发 increment 事件
emit('increment')
}
const reset = () => {
// 触发 update:count 事件,传递数据
emit('update:count', 0)
}
const sendData = () => {
// 传递复杂数据
emit('submit', {
id: 1,
name: '张三',
timestamp: Date.now()
})
}
</script>
父组件监听事件:
vue
<!-- ParentComponent.vue -->
<template>
<div>
<h2>父组件</h2>
<p>当前计数: {{ count }}</p>
<!-- 监听子组件事件 -->
<ChildComponent
@increment="handleIncrement"
@update:count="handleUpdateCount"
@submit="handleSubmit"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const count = ref(0)
// 事件处理函数
const handleIncrement = () => {
count.value++
console.log('计数增加')
}
const handleUpdateCount = (newValue) => {
count.value = newValue
console.log('计数更新为:', newValue)
}
const handleSubmit = (data) => {
console.log('收到提交数据:', data)
alert(`收到数据: ${JSON.stringify(data)}`)
}
</script>
TypeScript 支持
vue
<!-- ChildComponent.vue -->
<script setup lang="ts">
// 带类型的事件定义
const emit = defineEmits<{
// 无参数事件
(e: 'increment'): void
// 带参数事件
(e: 'update:count', value: number): void
// 复杂参数事件
(e: 'submit', data: { id: number; name: string }): void
// 多个参数事件
(e: 'search', keyword: string, page: number): void
}>()
const sendData = () => {
emit('submit', { id: 1, name: '张三' })
// emit('submit', { id: 1 }) // ❌ 会报错,缺少 name
}
</script>
三、v-model 双向绑定
单 v-model
子组件:
vue
<!-- InputComponent.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps({
modelValue: {
type: String,
default: ''
}
})
defineEmits(['update:modelValue'])
</script>
父组件:
vue
<!-- ParentComponent.vue -->
<template>
<div>
<p>输入的内容: {{ text }}</p>
<!-- 使用 v-model -->
<InputComponent v-model="text" />
<!-- 等价于 -->
<!-- <InputComponent :model-value="text" @update:model-value="text = $event" /> -->
</div>
</template>
<script setup>
import { ref } from 'vue'
import InputComponent from './InputComponent.vue'
const text = ref('')
</script>
多 v-model
子组件:
vue
<!-- UserForm.vue -->
<template>
<div>
<input
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
placeholder="名"
/>
<input
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
placeholder="姓"
/>
</div>
</template>
<script setup>
defineProps({
firstName: String,
lastName: String
})
defineEmits(['update:firstName', 'update:lastName'])
</script>
父组件:
vue
<template>
<div>
<p>全名: {{ firstName }} {{ lastName }}</p>
<!-- 多个 v-model -->
<UserForm
v-model:first-name="firstName"
v-model:last-name="lastName"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import UserForm from './UserForm.vue'
const firstName = ref('张')
const lastName = ref('三')
</script>
自定义 v-model
vue
<!-- CustomCounter.vue -->
<template>
<div>
<button @click="decrement">-</button>
<span>{{ modelValue }}</span>
<button @click="increment">+</button>
</div>
</template>
<script setup>
const props = defineProps({
modelValue: {
type: Number,
default: 0
},
min: {
type: Number,
default: 0
},
max: {
type: Number,
default: 100
}
})
const emit = defineEmits(['update:modelValue'])
const increment = () => {
if (props.modelValue < props.max) {
emit('update:modelValue', props.modelValue + 1)
}
}
const decrement = () => {
if (props.modelValue > props.min) {
emit('update:modelValue', props.modelValue - 1)
}
}
</script>
四、插槽(Slots)
默认插槽
子组件:
vue
<!-- Card.vue -->
<template>
<div class="card">
<div class="card-header">
<slot name="header">
默认头部
</slot>
</div>
<div class="card-body">
<!-- 默认插槽 -->
<slot>
默认内容
</slot>
</div>
<div class="card-footer">
<slot name="footer">
默认底部
</slot>
</div>
</div>
</template>
父组件:
vue
<template>
<Card>
<!-- 具名插槽 -->
<template #header>
<h3>自定义标题</h3>
</template>
<!-- 默认插槽 -->
<p>这是卡片内容</p>
<button>点击我</button>
<!-- 另一个具名插槽 -->
<template #footer>
<span>© 2024 版权所有</span>
</template>
</Card>
</template>
作用域插槽
子组件:
vue
<!-- ListComponent.vue -->
<template>
<ul>
<li v-for="(item, index) in items" :key="item.id">
<!-- 将数据传递给父组件 -->
<slot :item="item" :index="index">
默认显示: {{ item.name }}
</slot>
</li>
</ul>
</template>
<script setup>
defineProps({
items: {
type: Array,
default: () => []
}
})
</script>
父组件:
vue
<template>
<ListComponent :items="users">
<!-- 接收子组件传递的数据 -->
<template v-slot="{ item, index }">
<div>
{{ index + 1 }}. {{ item.name }} ({{ item.age }}岁)
<button @click="selectUser(item)">选择</button>
</div>
</template>
</ListComponent>
</template>
<script setup>
import { ref } from 'vue'
import ListComponent from './ListComponent.vue'
const users = ref([
{ id: 1, name: '张三', age: 25 },
{ id: 2, name: '李四', age: 30 },
{ id: 3, name: '王五', age: 28 }
])
const selectUser = (user) => {
console.log('选中用户:', user)
}
</script>
五、provide/inject(跨层级传递)
基本使用
祖先组件(提供数据):
vue
<!-- AncestorComponent.vue -->
<template>
<div>
<h2>祖先组件</h2>
<ParentComponent />
</div>
</template>
<script setup>
import { provide, ref, reactive } from 'vue'
import ParentComponent from './ParentComponent.vue'
// 提供响应式数据
const count = ref(0)
const user = reactive({
name: '张三',
role: '管理员'
})
// 提供方法
const updateCount = (newValue) => {
count.value = newValue
}
// 使用 provide
provide('globalCount', count)
provide('userInfo', user)
provide('updateCount', updateCount)
provide('appName', '我的应用') // 非响应式数据
</script>
后代组件(注入数据):
vue
<!-- ChildComponent.vue -->
<template>
<div>
<h3>子孙组件</h3>
<p>全局计数: {{ globalCount }}</p>
<p>用户: {{ userInfo.name }} ({{ userInfo.role }})</p>
<p>应用名称: {{ appName }}</p>
<button @click="updateCount(globalCount + 1)">增加计数</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
// 注入数据
const globalCount = inject('globalCount')
const userInfo = inject('userInfo')
const updateCount = inject('updateCount')
const appName = inject('appName', '默认应用') // 提供默认值
// 如果没有提供,可以设置默认值
const optionalData = inject('optionalData', '默认值')
// 确保存在
const requiredData = inject('requiredData')
if (!requiredData) {
throw new Error('requiredData 未提供')
}
</script>
TypeScript 支持
vue
<script setup lang="ts">
import { inject, Ref } from 'vue'
// 注入带类型的数据
const count = inject<Ref<number>>('count')
const user = inject<User>('user')
const updateUser = inject<(user: User) => void>('updateUser')
// 带默认值的注入
const title = inject<string>('title', '默认标题')
</script>
六、ref 访问子组件实例
访问子组件
父组件:
vue
<!-- ParentComponent.vue -->
<template>
<div>
<h2>父组件</h2>
<!-- 通过 ref 获取子组件实例 -->
<ChildComponent ref="childRef" />
<button @click="callChildMethod">调用子组件方法</button>
<button @click="getChildData">获取子组件数据</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
// 创建 ref
const childRef = ref(null)
onMounted(() => {
// 确保子组件已挂载
console.log('子组件实例:', childRef.value)
})
// 调用子组件方法
const callChildMethod = () => {
if (childRef.value) {
childRef.value.sayHello()
childRef.value.increment()
}
}
// 获取子组件数据
const getChildData = () => {
if (childRef.value) {
console.log('子组件计数:', childRef.value.count)
console.log('子组件信息:', childRef.value.getInfo())
}
}
</script>
子组件:
vue
<!-- ChildComponent.vue -->
<template>
<div>
<h3>子组件</h3>
<p>计数: {{ count }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
// 子组件方法
const sayHello = () => {
console.log('Hello from child!')
alert('子组件说你好!')
}
const increment = () => {
count.value++
console.log('计数增加到:', count.value)
}
const getInfo = () => {
return {
count: count.value,
timestamp: Date.now()
}
}
// 通过 defineExpose 暴露给父组件
defineExpose({
count,
sayHello,
increment,
getInfo
})
</script>
访问多个子组件
vue
<template>
<div>
<!-- 通过 v-for 和 ref 数组 -->
<ChildComponent
v-for="i in 3"
:key="i"
:ref="(el) => { if (el) childRefs[i] = el }"
/>
<button @click="callAllChildren">调用所有子组件</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const childRefs = ref([])
const callAllChildren = () => {
childRefs.value.forEach((child, index) => {
if (child) {
console.log(`调用子组件 ${index}:`)
child.sayHello()
}
})
}
</script>
七、实际项目示例
综合案例:任务管理应用
父组件:
vue
<!-- TaskManager.vue -->
<template>
<div class="task-manager">
<h1>任务管理器</h1>
<!-- 添加任务 -->
<TaskForm @add-task="addTask" />
<!-- 任务列表 -->
<TaskList
:tasks="tasks"
@update-task="updateTask"
@delete-task="deleteTask"
v-model:filter="filter"
/>
<!-- 任务统计 -->
<TaskStats :tasks="tasks" />
</div>
</template>
<script setup>
import { ref, computed, provide } from 'vue'
import TaskForm from './TaskForm.vue'
import TaskList from './TaskList.vue'
import TaskStats from './TaskStats.vue'
// 数据
const tasks = ref([
{ id: 1, title: '学习 Vue 3', completed: true },
{ id: 2, title: '写项目文档', completed: false },
{ id: 3, title: '代码评审', completed: false }
])
const filter = ref('all')
// 添加任务
const addTask = (newTask) => {
tasks.value.push({
id: Date.now(),
title: newTask,
completed: false
})
}
// 更新任务
const updateTask = (taskId, updates) => {
const index = tasks.value.findIndex(t => t.id === taskId)
if (index !== -1) {
tasks.value[index] = { ...tasks.value[index], ...updates }
}
}
// 删除任务
const deleteTask = (taskId) => {
tasks.value = tasks.value.filter(t => t.id !== taskId)
}
// 通过 provide 共享数据
provide('taskContext', {
tasks,
filter,
updateTask,
deleteTask
})
</script>
子组件示例:TaskForm.vue
vue
<template>
<form @submit.prevent="handleSubmit" class="task-form">
<input
v-model="newTask"
placeholder="输入新任务..."
ref="inputRef"
/>
<button type="submit">添加</button>
</form>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const emit = defineEmits(['add-task'])
const newTask = ref('')
const inputRef = ref(null)
onMounted(() => {
// 自动聚焦
inputRef.value?.focus()
})
const handleSubmit = () => {
if (newTask.value.trim()) {
emit('add-task', newTask.value.trim())
newTask.value = ''
}
}
</script>
孙组件示例:TaskItem.vue
vue
<template>
<li class="task-item" :class="{ completed: task.completed }">
<input
type="checkbox"
:checked="task.completed"
@change="toggleComplete"
/>
<span>{{ task.title }}</span>
<button @click="deleteTask">删除</button>
</li>
</template>
<script setup>
import { inject } from 'vue'
const props = defineProps({
task: {
type: Object,
required: true
}
})
// 通过 inject 获取方法
const { updateTask, deleteTask } = inject('taskContext')
const toggleComplete = () => {
updateTask(props.task.id, { completed: !props.task.completed })
}
</script>
📊 参数传递方式选择指南
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 父向子传递数据 | Props | 简单、清晰、单向数据流 |
| 子向父传递事件 | 自定义事件 | 明确的数据流向 |
| 表单双向绑定 | v-model | 语法糖,简洁易用 |
| 传递UI结构 | 插槽 | 灵活的内容分发 |
| 跨层级传递 | provide/inject | 避免 props 逐层传递 |
| 访问子组件 | ref + defineExpose | 需要直接操作子组件 |
| 复杂状态共享 | Pinia/Vuex | 大型应用状态管理 |
最佳实践建议
-
Props 向下,事件向上
- 父组件通过 props 传递数据给子组件
- 子组件通过事件通知父组件状态变化
-
避免直接修改 Props
javascript// ❌ 错误 props.user.name = '新名字' // ✅ 正确 emit('update:user', { ...props.user, name: '新名字' }) -
保持 Props 的简洁性
- 避免传递复杂的嵌套对象
- 使用 computed 或 watch 处理复杂逻辑
-
为自定义事件使用描述性名称
- 使用
update:前缀支持 v-model - 使用 kebab-case 命名事件
- 使用
-
合理使用 ref
- 仅用于必要的 DOM 操作
- 避免过度使用破坏组件封装性
-
TypeScript 类型支持
- 始终为 props 和 emits 提供类型定义
- 使用接口定义复杂的数据结构
通过合理选择和组合这些参数传递方式,您可以构建出结构清晰、易于维护的 Vue 3 应用。