父子组件参数传递

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 大型应用状态管理

最佳实践建议

  1. Props 向下,事件向上

    • 父组件通过 props 传递数据给子组件
    • 子组件通过事件通知父组件状态变化
  2. 避免直接修改 Props

    javascript 复制代码
    // ❌ 错误
    props.user.name = '新名字'
    
    // ✅ 正确
    emit('update:user', { ...props.user, name: '新名字' })
  3. 保持 Props 的简洁性

    • 避免传递复杂的嵌套对象
    • 使用 computed 或 watch 处理复杂逻辑
  4. 为自定义事件使用描述性名称

    • 使用 update: 前缀支持 v-model
    • 使用 kebab-case 命名事件
  5. 合理使用 ref

    • 仅用于必要的 DOM 操作
    • 避免过度使用破坏组件封装性
  6. TypeScript 类型支持

    • 始终为 props 和 emits 提供类型定义
    • 使用接口定义复杂的数据结构

通过合理选择和组合这些参数传递方式,您可以构建出结构清晰、易于维护的 Vue 3 应用。

相关推荐
Pedantic1 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘1 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆1 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师2 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆2 小时前
VSCode自动格式化三要素
前端
爱勇宝3 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
kyriewen4 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
user20585561518136 小时前
Windows 项目安装时报 `node-sass` 错误,如何快速处理
前端
LiaCode6 小时前
Redis 在生产项目的使用
前端·后端