父子组件参数传递

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 应用。

相关推荐
happymaker06262 小时前
web前端学习日记——DAY06(js基础语法与数据类型)
前端·javascript·学习
不会写DN2 小时前
JS 最常用的性能优化 防抖和节流
开发语言·javascript·ecmascript
紫_龙2 小时前
最新版vue3+TypeScript开发入门到实战教程之生命周期函数
前端·javascript·typescript
小江的记录本2 小时前
【反射】Java反射 全方位知识体系(附 应用场景 + 《八股文常考面试题》)
java·开发语言·前端·后端·python·spring·面试
孟陬2 小时前
国外技术周刊 #4:这38条阅读法则改变了我的人生、男人似乎只追求四件事……
前端·人工智能·后端
工边页字2 小时前
cursor接上figma mcp ,图形图像模式傻瓜式教学(包教包会版)
前端·人工智能·ai编程
callJJ2 小时前
Ant Design Table 批量操作踩坑总结 —— 从三个 Bug 看前端表格开发的共性问题
java·前端·经验分享·bug·管理系统
我去流水了2 小时前
【独家免费】【亲测】在linux下嵌入式linux的web http服务【Get、Post】,移植mongoose,post上传文件
linux·运维·前端
Mintopia2 小时前
世界头部大厂的研发如何使用 AI-Coding?
前端