目录
- 一、通信方式概览
- 二、详细说明
- [1. Props(父传子)](#1. Props(父传子))
- [2. Props + 函数(子传父)](#2. Props + 函数(子传父))
- [3. 自定义事件(emit)- 子传父](#3. 自定义事件(emit)- 子传父)
- Q1:自定义事件和原生事件的区别?
- [Q2:defineEmits 的作用是什么?](#Q2:defineEmits 的作用是什么?)
- [Q3:emit 可以传多个参数吗?](#Q3:emit 可以传多个参数吗?)
- Q4:子组件如何修改父组件的数据?
- [4. v-model(父子双向绑定)](#4. v-model(父子双向绑定))
- [5. provide / inject(祖孙通信)](#5. provide / inject(祖孙通信))
- [6. mitt / eventBus(任意组件通信)](#6. mitt / eventBus(任意组件通信))
- [7. Pinia / Vuex(全局状态管理)](#7. Pinia / Vuex(全局状态管理))
- [8. refs / parent(直接访问)](#8. refs / parent(直接访问))
- [9. slot 插槽(父传模板)](#9. slot 插槽(父传模板))
- [10. attrs / listeners(属性透传)](#10. attrs / listeners(属性透传))
- 三、选择建议
- 四、面试速记卡片
一、通信方式概览
| 方式 | 方向 | 适用场景 | 复杂度 |
|---|---|---|---|
| Props | 父→子 | 最基础,父传数据给子 | ⭐ |
| Props + 函数 | 子→父 | 子组件需要通知父组件 | ⭐⭐ |
| 自定义事件 (emit) | 子→父 | 子组件向父组件传数据 | ⭐⭐ |
| v-model | 双向 | 父子组件双向绑定 | ⭐⭐ |
| provide/inject | 祖→孙 | 跨多级组件传递数据 | ⭐⭐⭐ |
| mitt / eventBus | 任意 | 任意组件间通信 | ⭐⭐⭐ |
| Pinia / Vuex | 任意 | 大型应用全局状态管理 | ⭐⭐⭐⭐ |
| refs / parent | 直接访问 | 直接调用子/父组件方法 | ⭐⭐ |
| slot 插槽 | 父→子 | 父组件传递模板内容 | ⭐⭐ |
| attrs / listeners | 祖→孙 | 透传属性和事件 | ⭐⭐⭐ |
二、详细说明
1. Props(父传子)
最基础的通信方式,父组件通过属性向子组件传递数据。
- 父组件在调用子组件的地方绑定数据,
- 子组件通过 defineProps 接收父组件传来的数据。
javascript
<!-- 父组件 Father.vue -->
<template>
<Child :name="userName" :age="18" />
</template>
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const userName = ref('张三')
</script>
<!-- 子组件 Child.vue -->
<script setup>
// 方式1:声明接收
const props = defineProps(['name', 'age'])
// 方式2:带类型声明
const props = defineProps({
name: String,
age: {
type: Number,
default: 0,
required: true
}
})
// 使用 props.name
console.log(props.name)
</script>
注意事项:
-
Props 是只读的,子组件不能直接修改
-
如果需要修改,应该通过 emit 通知父组件修改
2. Props + 函数(子传父)
父组件传递一个函数给子组件,子组件调用该函数并传参,实现子传父。
- 父组件在调用子组件的地方传入一个函数,
- 子组件通过 defineProps 接收,
- 子组件调用这个方法并传参 ,把数据传回给父组件。
javascript
<!-- 父组件 -->
<template>
<Child :onSendMessage="handleMessage" />
</template>
<script setup>
import Child from './Child.vue'
function handleMessage(msg) {
console.log('收到子组件的消息:', msg)
}
</script>
<!-- 子组件 -->
<script setup>
const props = defineProps(['onSendMessage'])
function sendToParent() {
props.onSendMessage('Hello 父组件')
}
</script>
3. 自定义事件(emit)- 子传父
Vue 推荐的标准子传父方式,子组件通过 $emit 触发事件,父组件监听事件。
- 父:给子组件绑定事件名,用@自定义事件名
- 子:用defineEmits 声明事件,用 emit(名字, 数据) 触发
- 父:自动收到数据
javascript
<!-- 父组件 -->
<template>
<Child @send-message="handleMessage" />
</template>
<script setup>
import Child from './Child.vue'
function handleMessage(msg) {
console.log('收到消息:', msg)
}
</script>
<!-- 子组件 -->
<script setup>
// 声明事件
const emit = defineEmits(['send-message'])
function sendToParent() {
// 触发事件,传递数据
emit('send-message', 'Hello 父组件')
}
</script>
Q1:自定义事件和原生事件的区别?
| 对比 | 原生事件 | 自定义事件 |
|---|---|---|
| 事件名 | 固定(click、input等) | 任意名称 |
| 触发方式 | 用户交互自动触发 | 手动调用 emit 触发 |
| $event 对象 | DOM 事件对象 | emit 传递的数据 |
| 使用场景 | 监听 DOM 操作 | 组件间通信 |
Q2:defineEmits 的作用是什么?
defineEmits是 Vue 3 中用于声明自定义事件的函数。它有两个作用:
类型声明:让 TypeScript 知道有哪些事件
运行时验证:可以验证事件参数格式(开发环境)
返回值:返回 emit 函数,用于触发事件
Q3:emit 可以传多个参数吗?
可以,但只推荐传一个参数。因为:
如果传多个,只有第一个参数会作为
$event传递其他参数会被忽略,导致数据丢失
推荐用对象或数组包裹多个值
javascript
// ❌ 不推荐
emit('update', name, age, email)
// ✅ 推荐
emit('update', { name, age, email })
Q4:子组件如何修改父组件的数据?
Vue 是单向数据流,子组件不能直接修改父组件的数据。正确做法是:
子组件通过
emit触发事件,把要修改的值传出去父组件监听事件,在回调中修改自己的数据
修改后的数据通过
props传回子组件
4. v-model(父子双向绑定)
v-model 是双向绑定的语法糖,本质是 props + emit。
javascript
<!-- 父组件 -->
<template>
<!-- 写法1:v-model -->
<CustomInput v-model="username" />
<!-- 写法2:完整写法(v-model 的本质) -->
<CustomInput
:modelValue="username"
@update:modelValue="username = $event"
/>
<!-- 写法3:多个 v-model 绑定 -->
<CustomForm
v-model:firstName="firstName"
v-model:lastName="lastName"
/>
</template>
<script setup>
import { ref } from 'vue'
const username = ref('')
const firstName = ref('')
const lastName = ref('')
</script>
<!-- 子组件 CustomInput.vue -->
<template>
<input
type="text"
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<!-- 支持多个 v-model 的子组件 -->
<template>
<input :value="firstName" @input="emit('update:firstName', $event.target.value)" />
<input :value="lastName" @input="emit('update:lastName', $event.target.value)" />
</template>
<script setup>
defineProps(['firstName', 'lastName'])
const emit = defineEmits(['update:firstName', 'update:lastName'])
</script>
5. provide / inject(祖孙通信)
解决跨多级组件通信问题,祖先组件提供数据,后代组件注入使用。
javascript
<!-- 祖先组件 GrandParent.vue -->
<script setup>
import { provide, ref, reactive } from 'vue'
// 提供普通数据
const message = ref('Hello')
provide('message', message)
// 提供响应式数据和方法
const count = ref(0)
function increment() {
count.value++
}
provide('countContext', { count, increment })
</script>
<!-- 中间组件 - 不需要任何处理,透传即可 -->
<!-- 孙组件 GrandChild.vue -->
<script setup>
import { inject } from 'vue'
// 注入数据
const message = inject('message')
const { count, increment } = inject('countContext')
// 也可以提供默认值
const optional = inject('notExists', '默认值')
// 使用函数式默认值
const data = inject('key', () => ({ name: '默认' }))
</script>
注意事项:
-
provide 的数据建议使用
ref或reactive保持响应式 -
inject 的数据在子组件中可以直接使用
-
适合祖孙通信,中间组件不需要知道数据的存在
6. mitt / eventBus(任意组件通信)
轻量级的事件总线,适合中小型项目的任意组件通信。
bash
javascript
npm i mitt
typescript
javascript
// utils/emitter.ts
import mitt from 'mitt'
const emitter = mitt()
export default emitter
vue
javascript
<!-- 发送数据组件 Sender.vue -->
<script setup>
import emitter from '@/utils/emitter'
import { ref } from 'vue'
const message = ref('Hello')
function sendMessage() {
emitter.emit('custom-event', message.value)
}
</script>
<!-- 接收数据组件 Receiver.vue -->
<script setup>
import emitter from '@/utils/emitter'
import { onUnmounted } from 'vue'
function handleMessage(data) {
console.log('收到消息:', data)
}
// 监听事件
emitter.on('custom-event', handleMessage)
// 必须解绑,防止内存泄漏!
onUnmounted(() => {
emitter.off('custom-event', handleMessage)
})
</script>
<!-- 清除所有事件 -->
emitter.all.clear()
mitt API 速查:
| 方法 | 作用 |
|---|---|
emitter.on(event, handler) |
监听事件 |
emitter.off(event, handler) |
解绑事件(必须指定 handler) |
emitter.emit(event, data) |
触发事件 |
emitter.all.clear() |
清除所有事件 |
7. Pinia / Vuex(全局状态管理)
官方推荐的状态管理方案,适合大型应用。
bash
javascript
npm i pinia
typescript
javascript
// stores/user.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
// state:存放数据
state: () => ({
name: '张三',
age: 18,
token: ''
}),
// getters:计算属性
getters: {
doubleAge: (state) => state.age * 2,
userInfo: (state) => `${state.name} - ${state.age}岁`
},
// actions:业务逻辑(同步/异步)
actions: {
// 同步修改
setName(name: string) {
this.name = name
},
// 异步修改
async login(username: string, password: string) {
const res = await api.login(username, password)
this.token = res.data.token
this.name = res.data.name
},
// 批量修改
updateUser(data: Partial<{ name: string; age: number }>) {
Object.assign(this.$state, data)
}
}
})
vue
javascript
<!-- 组件中使用 -->
<script setup>
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
// 解构 state 需要 storeToRefs 保持响应式
const { name, age, doubleAge } = storeToRefs(userStore)
// actions 可以直接解构
const { setName, login } = userStore
// 使用
function updateUser() {
setName('李四')
// 或
userStore.name = '王五'
}
</script>
<template>
<div>
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
<p>两倍年龄:{{ doubleAge }}</p>
<button @click="setName('新名字')">修改</button>
</div>
</template>
Pinia 核心三概念:
| 概念 | 作用 | 类比 |
|---|---|---|
| state | 存放共享数据 | 组件中的 data |
| getters | 计算/过滤数据 | 组件中的 computed |
| actions | 业务逻辑(同步+异步) | 组件中的 methods |
8. refs / parent(直接访问)
直接获取组件实例,调用其方法或访问其数据。
vue
javascript
<!-- 父组件 -->
<template>
<Child ref="childRef" />
<button @click="callChildMethod">调用子组件方法</button>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
// 获取子组件实例
const childRef = ref(null)
function callChildMethod() {
// 调用子组件的方法
childRef.value.sayHello()
// 访问子组件的数据
console.log(childRef.value.childData)
}
// 访问父组件
function getParent() {
console.log(childRef.value.$parent)
}
</script>
<!-- 子组件 -->
<script setup>
import { ref } from 'vue'
const childData = ref('子组件数据')
function sayHello() {
console.log('Hello from child')
}
// 暴露给父组件(<script setup> 默认不暴露)
defineExpose({
childData,
sayHello
})
</script>
9. slot 插槽(父传模板)
父组件向子组件传递模板内容。
vue
javascript
<!-- 父组件 -->
<template>
<Card>
<!-- 默认插槽 -->
<p>这是卡片内容</p>
<!-- 具名插槽 -->
<template #header>
<h2>卡片标题</h2>
</template>
<template #footer>
<button>确定</button>
</template>
<!-- 作用域插槽:子组件向父组件传递数据 -->
<template #item="{ item, index }">
<div>{{ index }} - {{ item.name }}</div>
</template>
</Card>
</template>
<!-- 子组件 Card.vue -->
<template>
<div class="card">
<div class="header">
<slot name="header">默认标题</slot>
</div>
<div class="content">
<slot>默认内容</slot>
</div>
<div class="footer">
<slot name="footer">默认底部</slot>
</div>
<!-- 作用域插槽:传递数据给父组件 -->
<div v-for="(item, index) in items" :key="index">
<slot name="item" :item="item" :index="index"></slot>
</div>
</div>
</template>
10. attrs / listeners(属性透传)
自动透传父组件传入但子组件未声明的属性和事件。
vue
javascript
<!-- 父组件 -->
<template>
<BaseButton
class="big-btn"
:disabled="isDisabled"
@click="handleClick"
data-id="123"
>
按钮
</BaseButton>
</template>
<!-- 子组件 BaseButton.vue -->
<template>
<!-- $attrs 会自动绑定到根元素 -->
<button :class="$attrs.class" v-bind="$attrs">
<slot />
</button>
</template>
<script setup>
// 如果声明了 props,对应的属性就不会进入 $attrs
// const props = defineProps(['disabled'])
// 获取所有透传属性
const attrs = useAttrs()
console.log(attrs) // { class: 'big-btn', onClick: fn, dataId: '123' }
// 禁用自动透传(Vue 3.3+)
defineOptions({
inheritAttrs: false
})
</script>
三、选择建议
| 场景 | 推荐方案 |
|---|---|
| 父子组件简单传值 | Props / emit |
| 父子组件双向绑定 | v-model |
| 祖孙组件跨级传递 | provide/inject |
| 任意组件通信(小型项目) | mitt |
| 任意组件通信(大型项目) | Pinia |
| 需要访问子组件实例 | $refs |
| 父组件传递模板给子组件 | slot |
| 封装高阶组件/透传属性 | $attrs |
四、面试速记卡片
text
父子通信:
├── Props(父→子)
├── emit(子→父)
├── v-model(双向)
└── $refs(直接访问)
跨级通信:
├── provide/inject(祖→孙)
└── $attrs(属性透传)
全局通信:
├── mitt(轻量级事件总线)
└── Pinia/Vuex(状态管理)
模板传递:
└── slot(插槽)