vue 父子组件通信
1. Props - 父传子
父组件(Parent.vue)
<template>
<div>
<h2>父组件</h2>
<Child :msg="parentMsg" :count="count" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
// 父组件数据
const parentMsg = ref('我是父组件传递的消息')
const count = ref(100)
</script>
子组件(Child.vue)
<template>
<p>父组件传递的msg:{{ msg }}</p> <p>父组件传递的count:{{ count }}</p>
</template>
<script setup>
// const props = defineProps(['msg', 'count']) // 方式1:简洁声明(仅指定属性名)
const props = defineProps({ // 方式2:带类型校验(推荐)
msg: {
type: String,
required: true, // 是否必传
default: '默认消息' // 默认值(非必传时生效)
},
count: {
type: Number,
validator: (value) => { // 自定义校验
return value >= 0
}
}
})
console.log(props.msg, props.count) // 访问props数据(组合式API中props是响应式的)
</script>
注意:
defineProps在<script setup>中无需导入,直接使用。- Props 是单向数据流,子组件不能直接修改 props,若需修改,应通知父组件修改(或在子组件内用
ref接收 props 并作为本地数据)。
2. 自定义事件 (emit) - 子传父
子组件通过触发自定义事件,将数据传递给父组件,父组件通过v-on(简写@)监听事件。
子组件(Child.vue)
defineEmits:子组件声明事件名emit:子组件的实际触发事件,顺带传递数据(名字随便取,推荐短横线,因为HTML 里不区分大小写)
<template>
<div>
<h3>子组件</h3>
<button @click="sendDataToParent">向父组件传数据</button>
</div>
</template>
<script setup>
// 方式1:简洁声明(仅指定事件名)
// const emit = defineEmits(['child-click', 'send-data'])
// 方式2:带校验(推荐)
const emit = defineEmits({
'child-click': null, // 无参数校验
'send-data': (data) => { // 校验参数
if (data && data.content) {
return true
} else {
console.error('传递的数据格式错误')
return false
}
}
})
const sendDataToParent = () => {
// 触发自定义事件,可传递多个参数
emit('child-click')
emit('send-data', {
content: '我是子组件传递的数据',
num: 666
})
}
</script>
父组件(Parent.vue)
父组件的@事件名 = :监听子组件的事件,执行自己的逻辑
<template>
<p>子组件传递的数据:{{ childData.content }}</p>
<Child
@child-click="handleChildClick"
@send-data="handleSendData"
/>
</template>
<script setup>
...
const childData = ref({})
const handleChildClick = () => {
console.log('子组件触发了child-click事件') // 监听子组件的child-click事件
}
const handleSendData = (data) => {
childData.value = data // 监听子组件的send-data事件,接收参数
}
</script>
注意:
defineEmits在<script setup>中无需导入,直接使用。- 事件名推荐使用kebab-case(短横线命名),与 HTML 原生事件命名风格保持一致。
3. v-model 双向绑定 - 双向通信
v-model 可以在组件上使用以实现双向绑定。
v-model可以在组件上使用以实现双向绑定。从 Vue 3.4 开始,推荐的实现方式是使用 defineModel() 宏:
Child.vue
<script setup>
const model = defineModel()
function update() {
model.value++
}
</script>
<template>
<div>Parent bound v-model is: {{ model }}</div>
<button @click="update">Increment</button>
</template>
Parent.vue
<Child v-model="countModel" />
defineModel() 返回的值是一个 ref。它可以像其他 ref 一样被访问以及修改,能起到在父组件和当前变量之间的双向绑定的作用。这意味着你也可以用 v-model 把这个 ref 绑定到一个原生 input 元素上,在提供相同的 v-model 用法的同时轻松包装原生 input 元素:
<script setup>
const model = defineModel()
</script>
<template>
<input v-model="model" />
</template>
多个 v-model 绑定
利用刚才在 v-model 的参数小节中学到的指定参数与事件名的技巧,我们可以在单个组件实例上创建多个 v-model 双向绑定。组件上的每一个 v-model 都会同步不同的 prop,而无需额外的选项:
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>
<template>
<input type="text" v-model="firstName" />
<input type="text" v-model="lastName" />
</template>
详情请查看vue3官方文档: 组件 v-model | Vue.js
4. Refs 访问 - 父访问子
父组件通过ref获取子组件实例,从而访问子组件的属性和方法。子组件需通过defineExpose暴露需要被父组件访问的内容(<script setup>中组件内部的属性 / 方法默认是私有的)。
在这个场景里,ref会用两次(但作用不同):
| 用法 | 作用 |
|---|---|
const xxx = ref() | 定义响应式变量(比如存数字、字符串、对象,值存在.value里) |
<Child ref="xxx" /> | 给组件 / 元素 “贴标签”,通过xxx.value能拿到组件实例 / 元素 DOM 对象 |
子组件(Child.vue)
先写子组件,重点是 “定义变量 / 方法 + 用 defineExpose 暴露”:
<script setup>
import { ref } from 'vue'
const childNum = ref(0) // 1. 用ref定义子组件自己的响应式变量(这是ref的第一个用法:定义响应式数据)
const addNum = () => { // 2. 定义子组件自己的方法
childNum.value++
}
defineExpose({ // 3.用defineExpose暴露想让父组件访问的内容(变量/方法)
childNum,
addNum
})
</script>
父组件(Parent.vue)
父组件的核心是 “给子组件贴 ref 标签 + 通过 ref 拿到子组件实例 + 访问暴露的内容”。ref 变量名必须和模板里的 ref 标签名一致 !
<template>
<div>
<!-- 1. 给子组件贴ref标签(这是ref的第二个用法:标记组件) 标签名是childRef,父组件通过这个名字找子组件-->
<Child ref="childRef" />
<button @click="accessChild">点击访问子组件的内容</button>
<p>父组件拿到的子组件数字:{{ childNumFromSon }}</p>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
// 2. 定义ref变量(第一个用法),和子组件的ref标签名一致! 这个变量是用来存子组件实例的,初始值null(还没找到子组件)
const childRef = ref(null)
const childNumFromSon = ref(0)// 父组件自己的变量,用来存从子组件拿到的数字
// 3. 定义方法:访问子组件的内容
const accessChild = () => {
// 关键:通过childRef.value拿到子组件实例! 只有组件挂载后,childRef.value才不是null!
const sonInstance = childRef.value
// 4. 访问子组件暴露的变量(子实例上的 ref, Vue 自动解包)
childNumFromSon.value = sonInstance.childNum
// 5. 调用子组件暴露的方法
sonInstance.addNum() // 调用后子组件的childNum会+1
}
// 【避坑】可选:挂载后验证(组件挂载完成后,childRef才会指向子组件)
onMounted(() => {
console.log('组件挂载后,子组件实例:', childRef.value)
// 打印能看到子组件暴露的childNum和addNum
})
</script>
细节:
1、为什么childRef.value一开始是null?
Vue 组件渲染是 “异步” 的:父组件先创建childRef = ref(null),然后才开始渲染子组件;
只有等子组件挂载完成(onMounted 钩子触发后),childRef.value才会指向子组件实例;
正确做法:在onMounted里访问;在按钮点击、定时器等 “异步操作” 里访问(用户点击时组件肯定挂载好了)。
2、子组件为什么要写 defineExpose?
<script setup>是 Vue3 的 “语法糖”,里面定义的变量 / 方法默认是 “私有” 的(保护组件封装性);
defineExpose的作用就是 “把私有内容公开给父组件”,想让父组件访问什么,就暴露什么。
核心区别对比表
| 方式 | 数据流向 | 核心作用 | 组件封装性 | 推荐优先级 | 核心特点 |
|---|---|---|---|---|---|
| Props(父传子) | 父 → 子 | 父给子传递初始化 / 配置性数据 | 高(单向) | 最高 | 子只能读、不能改,要改只能通知父改(符合 Vue 单向数据流) |
| 自定义事件(子传父) | 子 → 父 | 子给父传递事件 / 状态变更数据 | 高 | 高 | 子只触发事件、不关心父怎么处理,父只监听事件、不侵入子内部 |
| Ref + defineExpose | 父 ← 子(主动) | 父主动访问 / 操作子的属性 / 方法 | 低(侵入) | 低 | 父能直接拿到子的实例,读属性、调方法,甚至改子的内部数据(慎用) |
5. Provide/Inject - 跨层传递(依赖注入)
provide/inject是 Vue3 专门解决跨层级组件通信的方案(比如:父 → 子 → 孙,甚至更深),跳过中间组件,避免 “Props 层层透传”(比如爷爷传父、父再传孙)的繁琐。

- provide(提供):上层组件(比如爷爷 / 父)“发布” 一些数据 / 方法,标记为 “可被后代组件获取”;
- inject(注入):下层组件(比如孙子 / 子)“订阅” 这些数据 / 方法,不用管中间层级(比如父组件)是否传递,直接拿过来用;
1、上层组件 —— 提供数据(provide)
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
provide() 函数接收两个参数。第一个参数被称为注入名,可以是一个字符串或是一个 Symbol; 后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。请确保 provide() 是在 setup() 同步调用的
<script setup>
import { provide, ref, reactive } from 'vue' ...
// 1. 提供「非响应式数据」(不推荐,下层拿不到更新)
provide('staticMsg', '我是父组件提供的静态消息')
// 2. 提供「响应式数据」(推荐!下层能实时拿到更新)
const dynamicCount = ref(0) // ref响应式
provide('dynamicCount', dynamicCount)
const userInfo = reactive({ name: '小明', age: 18 }) // reactive响应式
provide('userInfo', userInfo)
// 3. 提供「方法」(让下层组件能修改上层数据)
provide('updateCount', (value) => {
dynamicCount.value = value // 修改上层的响应式数据
})
</script>
2、下层组件 —— 注入数据(inject)
<script setup>
import { inject } from 'vue'
// 1. 注入数据:第一个参数是provide的key,第二个参数是「默认值」(上层没提供时生效)
const staticMsg = inject('staticMsg', '默认消息')
// 2. 注入响应式数据(拿到的是原始ref/reactive,自动响应更新)
const dynamicCount = inject('dynamicCount', ref(0)) // 给默认值也推荐用ref,保持类型一致
const userInfo = inject('userInfo', reactive({ name: '默认名', age: 0 }))
// 3. 注入上层提供的方法
const updateCount = inject('updateCount')
// 4. 直接修改reactive数据(因为是引用类型,上层也会同步更新)
const addUserAge = () => {
userInfo.age++
}
</script>
细节
(1)响应式是核心:上层提供非响应式数据,下层拿到后,上层再修改这个值,下层不会更新;上层提供响应式数据,下层拿到的是 “引用”,上层 / 下层修改,双方都会同步更新
✅ 推荐做法:所有需要更新的 provide 数据,都用响应式数据:ref或reactive包裹。
(2)默认值的写法(避免类型不一致)
(3)多人协作时,provide/inject 的 key 推荐抽成常量(避免拼写错误)
// constants/injectKeys.js
export const INJECT_KEYS = {
STATIC_MSG: 'staticMsg',
DYNAMIC_COUNT: 'dynamicCount',
UPDATE_COUNT: 'updateCount'
}
// 父组件
import { INJECT_KEYS } from './constants/injectKeys.js'
provide(INJECT_KEYS.DYNAMIC_COUNT, ref(0))
// 孙组件
import { INJECT_KEYS } from './constants/injectKeys.js'
const dynamicCount = inject(INJECT_KEYS.DYNAMIC_COUNT, ref(0))
(4)下层修改上层数据的两种方式(推荐第二种)
直接修改reactive数据(简单但不规范);上层提供方法,下层调用(推荐!符合 “单向数据流”)
适用场景(优先用)
- 跨多层级通信:比如 “爷爷 → 父 → 子 → 孙”,需要把数据传给孙组件,用 Props 会层层透传(父组件明明用不到,却要帮传),此时用
provide/inject最简洁; - 全局级通信(轻量):比如全局的主题配置(亮色 / 暗色)、用户登录状态、权限信息等,可在 App.vue(根组件)provide,所有页面 / 组件都能 inject;
注:如果是复杂的全局状态(比如购物车、用户信息),推荐用 Pinia/Vuex,而非
provide/inject。 - 组件库开发:比如封装一个表单组件库,表单根组件(Form)provide 配置,表单项组件(FormItem)inject 配置,无需用户手动传 Props。
对比(快速选型)
| 通信方式 | 数据流向 | 核心本质 | 组件封装性 | 适用层级 | 核心优势 | 核心劣势 | 典型适用场景 |
|---|---|---|---|---|---|---|---|
| Props | 父 → 子(单向) | 父给子传递配置 / 初始化数据,子只读不直接改 | 高 | 父子(直接) | 规范、单向数据流、易调试 | 跨层级需层层透传 | 父给子传基础数据(如标题、列表数据、开关状态) |
| emit | 子 → 父(单向) | 子触发事件传递数据,父监听事件处理,子不关心父的逻辑 | 高 | 父子(直接) | 解耦、符合单向数据流 | 跨层级需层层转发 | 子通知父状态变更(如按钮点击、表单输入、弹窗关闭) |
| ref | 父 ← 子(父主动访问) | 父通过 ref 拿到子实例,直接访问 / 调用子的属性 / 方法 | 低(侵入) | 父子(直接) | 父可主动操控子组件 | 破坏封装、调试难 | 父需主动控制子行为(如清空输入框、触发子组件刷新) |
| v-model | 父 ↔ 子(双向语法糖) | 本质是 Props(modelValue) + emit(update:modelValue),简化双向绑定 | 高 | 父子(直接) | 代码简洁、语义清晰 | 仅适用于 “值的双向绑定” | 表单类组件双向绑定(输入框、开关、滑块、下拉框) |
| provide /inject | 上层 → 下层(跨层级) | 上层提供数据 / 方法,下层注入使用,跳过中间层级 | 中 | 任意层级(跨层) | 解决多层透传问题 | 数据来源不直观、易滥用 | 跨多层级通信(如主题配置、用户登录状态)、组件库开发 |
更多推荐


所有评论(0)