父传子全解析:从基础到实战,新手也能零踩坑
在 Vue3 组件化开发中,父传子是最基础、最常用的组件通信方式,也是新手入门组件通信的第一步。无论是传递简单的字符串、数字,还是复杂的对象、数组,甚至是方法,父传子都有清晰、规范的实现方式。
不同于 Vue2 选项式 API 中 props 的写法,Vue3 组合式 API(
一、核心原理:单向数据流 + Props 传值
Vue3 父传子的核心逻辑只有两个关键词:Props 和 单向数据流。
- Props:父组件通过在子组件标签上绑定属性(类似 HTML 标签属性),将数据传递给子组件;子组件通过定义 props,接收父组件传递过来的数据,相当于子组件的「输入参数」。
- 单向数据流:数据只能从父组件流向子组件,子组件不能直接修改父组件传递过来的 props 数据(否则会报错)。如果子组件需要修改 props 数据,必须通过子传父的方式,通知父组件修改原始数据。
记住一句话:Props 是只读的,修改需找父组件。这是 Vue 组件通信的核心规范,也是避免数据混乱的关键。
父传子的核心流程(3步走):
- 父组件:在使用子组件的标签上,通过
:属性名="要传递的数据"绑定数据; - 子组件:通过
defineProps定义要接收的 props(声明属性名和类型,可选但推荐); - 子组件:在模板或脚本中,直接使用 props 中的数据(无需额外导入,直接通过 props.属性名 或 直接写属性名使用)。
二、基础用法:最简洁的父传子实现(必学)
我们用一个「父组件传递基本数据,子组件展示」的简单案例,讲解最基础的父传子写法,代码可直接复制到项目中运行,零门槛上手。
1. 父组件(Parent.vue):绑定数据并传递
<template>
<div class="parent">
<h3>我是父组件</h3>
<p>父组件的基本数据:{{ parentName }}、{{ parentAge }}</p>
<p>父组件的数组:{{ parentList.join('、') }}</p>
<p>父组件的对象:{{ parentObj.name }} - {{ parentObj.gender }}</p>
<!-- 1. 核心:在子组件标签上,通过 :属性名 绑定要传递的数据 -->
<Child
:name="parentName" // 传递字符串
:age="parentAge" // 传递数字
:list="parentList" // 传递数组
:user-info="parentObj" // 传递对象(推荐用短横线命名)
/>
</div>
</template>
<script setup>
// 引入子组件(Vue3 <script setup> 中,引入后可直接在模板中使用)
import Child from './Child.vue'
import { ref, reactive } from 'vue'
// 父组件要传递的数据(涵盖基本类型、数组、对象)
const parentName = ref('张三') // 字符串
const parentAge = ref(25) // 数字
const parentList = ref(['苹果', '香蕉', '橙子']) // 数组
const parentObj = reactive({ // 对象
name: '李四',
gender: '男',
age: 30
})
</script>
2. 子组件(Child.vue):定义Props并使用
<template>
<div class="child">
<h4>我是子组件(接收父组件传递的数据)</h4>
<p>接收的字符串:{{ name }}</p>
<p>接收的数字:{{ age }} 岁</p>
<p>接收的数组:{{ list.join('、') }}</p>
<p>接收的对象:{{ userInfo.name }}({{ userInfo.gender }})</p>
</div>
</template>
<script setup>
// 2. 核心:通过 defineProps 定义要接收的 props
// 写法1:数组形式(简单场景,只声明属性名,不限制类型)
// const props = defineProps(['name', 'age', 'list', 'userInfo'])
// 写法2:对象形式(推荐,可限制类型、设置默认值、必填校验)
const props = defineProps({
// 字符串类型
name: {
type: String,
default: '默认用户名' // 默认值(父组件未传递时使用)
},
// 数字类型
age: {
type: Number,
default: 18
},
// 数组类型(注意:数组/对象的默认值必须用函数返回,避免复用污染)
list: {
type: Array,
default: () => [] // 数组默认值:返回空数组
},
// 对象类型(同理,默认值用函数返回)
userInfo: {
type: Object,
default: () => ({}) // 对象默认值:返回空对象
}
})
// 3. 在脚本中使用 props 数据(通过 props.属性名)
console.log('脚本中使用props:', props.name, props.age)
</script>
3. 基础细节说明(新手必看)
-
defineProps是 Vue3 内置宏,无需导入,可直接在 - 父组件传递数据时,属性名推荐用
kebab-case(短横线命名),比如:user-info,子组件接收时用camelCase(小驼峰命名),比如userInfo,Vue 会自动做转换; - 数组/对象类型的 props,默认值必须用
函数返回(比如default: () => []),否则多个子组件会复用同一个默认值,导致数据污染; - 子组件模板中可直接使用 props 的属性名(比如
{{ name }}),脚本中必须通过props.属性名使用(比如props.name)。
三、进阶用法:优化父传子的体验(实战常用)
基础用法能满足简单场景,但在实际开发中,我们还会遇到「必填校验」「类型多可选」「props 数据转换」等需求,这部分进阶技巧能让你的代码更规范、更健壮,避免后续维护踩坑。
1. Props 校验:必填项 + 多类型 + 自定义校验
通过 defineProps 的对象形式,我们可以对 props 进行全方位校验,避免父组件传递错误类型、遗漏必填数据,提升代码可靠性。
<script setup>
const props = defineProps({
// 1. 必填项校验(required: true)
username: {
type: String,
required: true, // 父组件必须传递该属性,否则控制台报警告
default: '' // 注意:required: true 时,default 无效,可省略
},
// 2. 多类型校验(type 为数组)
id: {
type: [Number, String], // 允许父组件传递数字或字符串类型
default: 0
},
// 3. 自定义校验(validator 函数)
score: {
type: Number,
default: 0,
// 自定义校验规则:分数必须在 0-100 之间
validator: (value) => {
return value >= 0 && value <= 100
}
}
})
</script>
说明:校验失败时,Vue 会在控制台打印警告(不影响代码运行),但能帮助我们快速定位问题,尤其适合团队协作场景。
2. Props 数据转换:computed 处理 props 数据
子组件不能直接修改 props 数据,但可以通过 computed 对 props 数据进行转换、格式化,满足子组件的展示需求,不影响原始 props 数据。
<template>
<div class="child">
<p>父组件传递的分数:{{ score }}</p>
<p>转换后的等级:{{ scoreLevel }}</p>
<p>父组件传递的姓名(大写):{{ upperName }}</p>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
score: {
type: Number,
default: 0
},
name: {
type: String,
default: ''
}
})
// 对 props 分数进行转换:0-60 不及格,60-80 及格,80-100 优秀
const scoreLevel = computed(() => {
const { score } = props
if (score >= 80) return '优秀'
if (score >= 60) return '及格'
return '不及格'
})
// 对 props 姓名进行格式化:转为大写
const upperName = computed(() => {
return props.name.toUpperCase()
})
</script>
3. 传递方法:父组件给子组件传递回调函数
父传子不仅能传递数据,还能传递方法(回调函数)。核心用途:子组件通过调用父组件传递的方法,通知父组件修改数据(解决子组件不能直接修改 props 的问题)。
<!-- 父组件(Parent.vue) -->
<template>
<div class="parent">
<p>父组件计数器:{{ count }}</p>
<!-- 传递方法::方法名="父组件方法" -->
<Child
:count="count"
:addCount="handleAddCount" // 传递父组件的方法
/>
</div>
</template>
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const count = ref(0)
// 父组件的方法(将被传递给子组件)
const handleAddCount = () => {
count.value++
}
</script>
<!-- 子组件(Child.vue) -->
<template>
<div class="child">
<p>子组件接收的计数器:{{ count }}</p>
<!-- 调用父组件传递的方法 -->
<button @click="addCount">点击让父组件计数器+1</button>
</div>
</template>
<script setup>
const props = defineProps({
count: {
type: Number,
default: 0
},
// 声明接收父组件传递的方法(type 为 Function)
addCount: {
type: Function,
required: true
}
})
// 也可以在脚本中调用父组件的方法
const callParentMethod = () => {
props.addCount()
}
</script>
注意:传递方法时,父组件只需写 :addCount="handleAddCount"(不带括号),子组件调用时再带括号 addCount();如果父组件写 :addCount="handleAddCount()",会导致方法立即执行,而非传递方法本身。
4. 批量传递 props:v-bind 绑定对象
如果父组件需要给子组件传递多个 props,逐个绑定会比较繁琐,这时可以用 v-bind 批量绑定一个对象,子组件只需对应接收即可。
<!-- 父组件(Parent.vue) -->
<template>
<div class="parent">
<!-- 批量传递:v-bind="对象",等价于逐个绑定对象的属性 -->
<Child v-bind="userObj" />
</div>
</template>
<script setup>
import Child from './Child.vue'
import { reactive } from 'vue'
// 要批量传递的对象
const userObj = reactive({
name: '张三',
age: 25,
gender: '男',
address: '北京'
})
</script>
<!-- 子组件(Child.vue) -->
<script setup>
// 逐个接收父组件批量传递的 props,和普通 props 接收一致
const props = defineProps({
name: String,
age: Number,
gender: String,
address: String
})
</script>
四、实战场景:父传子的高频应用(贴合实际开发)
结合实际开发中的高频场景,补充 3 个常用案例,覆盖大部分父传子需求,直接套用即可。
场景1:父组件控制子组件弹窗显示/隐藏
<!-- 父组件(Parent.vue) -->
<template>
<div class="parent">
<button @click="visible = true">打开子组件弹窗</button>
<!-- 传递弹窗显示状态 + 关闭弹窗的方法 -->
<ChildModal
:visible="visible"
:closeModal="handleCloseModal"
/>
</div>
</template>
<script setup>
import ChildModal from './ChildModal.vue'
import { ref } from 'vue'
const visible = ref(false)
// 关闭弹窗的方法
const handleCloseModal = () => {
visible.value = false
}
</script>
<!-- 子组件(ChildModal.vue) -->
<template>
<div class="modal" v-if="visible">
<div class="modal-content">
<h4>子组件弹窗</h4>
<button @click="closeModal">关闭弹窗</button>
</div>
</div>
</template>
<script setup>
const props = defineProps({
visible: {
type: Boolean,
default: false
},
closeModal: {
type: Function,
required: true
}
})
</script>
场景2:父组件给子组件传递接口数据
实际开发中,父组件通常会请求接口,将接口返回的数据传递给子组件展示,这是最常见的场景之一。
<!-- 父组件(Parent.vue) -->
<template>
<div class="parent">
<!-- 加载中状态 -->
<div v-if="loading">加载中...</div>
<!-- 接口数据请求成功后,传递给子组件 -->
<ChildList :list="goodsList" v-else />
</div>
</template>
<script setup>
import ChildList from './ChildList.vue'
import { ref, onMounted } from 'vue'
const goodsList = ref([])
const loading = ref(false)
// 父组件请求接口
onMounted(async () => {
loading.value = true
try {
const res = await fetch('https://api.example.com/goods')
const data = await res.json()
goodsList.value = data.list // 接口返回的列表数据
} catch (err) {
console.error('接口请求失败:', err)
} finally {
loading.value = false
}
})
</script>
场景3:子组件复用,父组件传递不同配置
子组件复用是组件化开发的核心优势,通过父传子传递不同的配置,让同一个子组件实现不同的展示效果。
<!-- 父组件(Parent.vue) -->
<template>
<div class="parent">
<!-- 同一个子组件,传递不同配置,展示不同效果 -->
<Button
:text="按钮1"
:type="primary"
:disabled="false"
/>
<Button
:text="按钮2"
:type="default"
:disabled="true"
/>
</div>
</template>
<script setup>
import Button from './Button.vue'
</script>
<!-- 子组件(Button.vue) -->
<template>
<button
class="custom-btn"
:class="type === 'primary' ? 'btn-primary' : 'btn-default'"
:disabled="disabled"
>
{{ text }}
</button>
</template>
<script setup>
const props = defineProps({
text: {
type: String,
required: true
},
type: {
type: String,
default: 'default',
validator: (val) => {
return ['primary', 'default', 'danger'].includes(val)
}
},
disabled: {
type: Boolean,
default: false
}
})
</script>
五、常见坑点避坑指南(新手必看)
很多新手在写父传子时,会遇到「props 接收不到数据」「修改 props 报错」「方法传递后无法调用」等问题,以下是最常见的 5 个坑点,帮你快速避坑。
坑点1:父组件传递数据时,忘记加冒号(:)
错误写法:<Child name="parentName"></Child>(没有冒号,传递的是字符串 "parentName",而非父组件的 parentName 变量);
正确写法:<Child :name="parentName"></Child>(加冒号,传递的是父组件的变量)。
坑点2:子组件直接修改 props 数据
错误写法:props.name = '李四'(直接修改 props,会报错);
正确写法:通过父传子的方法,通知父组件修改原始数据(参考「传递方法」章节),或通过 computed 转换数据(不修改原始 props)。
坑点3:数组/对象 props 的默认值未用函数返回
错误写法:list: { type: Array, default: [] }(直接写数组,会导致多个子组件复用同一个数组,数据污染);
正确写法:list: { type: Array, default: () => [] }(用函数返回数组,每个子组件都会得到一个新的空数组)。
坑点4:传递方法时,父组件带了括号
错误写法:<Child :addCount="handleAddCount()"></Child>(方法立即执行,传递的是方法的返回值,而非方法本身);
正确写法:<Child :addCount="handleAddCount"></Child>(不带括号,传递方法本身)。
坑点5:props 命名大小写不一致
错误写法:父组件 :userInfo="parentObj",子组件接收 userinfo(小写 i);
正确写法:父组件用 kebab-case(:user-info),子组件用 camelCase(userInfo),或保持大小写一致(不推荐)。
六、总结:父传子核心要点回顾
Vue3 父传子的核心就是「Props 传值 + 单向数据流」,记住以下 4 个核心要点,就能应对所有父传子场景:
- 基础流程:父组件
:属性名="数据"绑定 → 子组件defineProps接收 → 子组件使用数据; - 核心规范:Props 是只读的,子组件不能直接修改,修改需通过父传子的方法通知父组件;
- 进阶技巧:props 校验提升可靠性,computed 转换数据,v-bind 批量传值,传递方法实现双向交互;
- 避坑关键:加冒号传递变量、不直接修改 props、数组/对象默认值用函数返回、传递方法不带括号。
父传子是 Vue3 组件通信中最基础、最常用的方式,掌握它之后,再学习子传父、跨层级通信(provide/inject)、全局通信(Pinia)会更轻松。