如有不对的或者遗漏的,欢迎指正!希望大家一起共同进步,共勉!
Vue3 组件通信方式
- props
- $emit
- v-model
- provide / inject
- expose / ref
- $attrs
- Pinia
- mitt
Vue3 通信使用写法
1. Props / Emits(父子通信)
原理 :父组件通过属性 (props)向子组件传递数据,子组件通过事件(emits)向父组件发送信息
html
<!-- 父组件 Parent.vue -->
<template>
<ChildComponent
:message="parentMessage"
@child-event="handleChildEvent"
/>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './Child.vue';
const parentMessage = ref('来自父组件的消息');
const handleChildEvent = (childData) => {
console.log('接收到子组件事件:', childData);
};
</script>
<!-- 子组件 Child.vue -->
<template>
<div>
<p>{{ message }}</p>
<button @click="sendMessage">发送消息到父组件</button>
</div>
</template>
<script setup>
defineProps({
message: {
type: String,
required: true
}
});
const emit = defineEmits(['child-event']);
const sendMessage = () => {
emit('child-event', '来自子组件的数据');
};
</script>
特点:
- 最基础、最常用的通信方式
- 单向数据流(父→子)
- 适用于直接父子关系组件
- 事件通信(子→父)
最佳实践:
- Props 命名使用 camelCase (驼峰命名法),在模板中使用 kebab-case(连字符命名法)
- 避免直接修改 Props(单向数据流原则)
2. v-model(双向绑定)
原理 :v-model 是 props/emits 的语法糖,简化双向绑定实现
html
<!-- 父组件 -->
<CustomInput v-model="inputValue" />
<!-- 子组件 CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps(['modelValue']);
defineEmits(['update:modelValue']);
</script>
多个 v-model:
html
<!-- 父组件 -->
<UserName
v-model:first-name="firstName"
v-model:last-name="lastName"
/>
<!-- 子组件 UserName.vue -->
<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: String,
lastName: String
});
defineEmits(['update:firstName', 'update:lastName']);
</script>
特点:
- 简洁的实现表单双向绑定
- Vue3 支持多个 v-model
- 实际上是 props + update:xxx 事件的组合
最佳实践:
- 优先用于表单输入组件
- 自定义修饰符处理(如 .trim , .number)
- 组件内部避免直接修改 prop 值
3. Provide / Inject(跨层级通信)
原理:祖先组件提供(provide)数据,后代组件注入(inject)使用
html
<!-- 祖先组件 App.vue -->
<template>
<ParentComponent />
</template>
<script setup>
import { provide, ref } from 'vue';
// 提供静态数据
provide('appName', 'Vue3 Application');
// 提供响应式数据
const user = ref({ name: 'John', age: 30 });
provide('user', user);
// 提供方法
const updateUser = (newData) => {
Object.assign(user.value, newData);
};
provide('updateUser', updateUser);
</script>
<!-- 后代组件 (任何后代) -->
<script setup>
import { inject } from 'vue';
// 注入数据
const appName = inject('appName');
const user = inject('user');
// 注入方法
const updateUser = inject('updateUser');
// 在需要的地方调用方法
const handleUpdate = () => {
updateUser({ age: 31 });
};
</script>
特点:
- 解决深层嵌套组件通信问题
- 避免"prop 逐级透传"问题
- 提供响应式数据支持
- 可提供方法给后代调用
最佳实践:
- 使用 Symbol 作为 key 避免命名冲突
js
// keys.js
export const APP_NAME = Symbol('appName');
export const USER_DATA = Symbol('userData');
// 提供时
import { APP_NAME } from './keys';
provide(APP_NAME, 'Vue3 App');
// 注入时
const appName = inject(APP_NAME);
- 为注入值设置默认值
js
const theme = inject('theme', 'light'); // 默认值为'light'
- 建议将提供逻辑封装在单独函数中
js
// useProvide.js
export function useProvideData() {
const data = ref(/* ... */);
provide('myData', data);
return data;
}
4. 模板引用(Ref)& defineExpose
原理:父组件通过 ref 获取子组件实例,调用子组件暴露的方法
html
<!-- 父组件 -->
<template>
<ChildComponent ref="childRef" />
<button @click="callChildMethod">调用子组件方法</button>
</template>
<script setup>
import { ref } from 'vue';
const childRef = ref(null);
const callChildMethod = () => {
childRef.value?.focusInput(); // 调用子组件方法
childRef.value?.resetForm(); // 调用另一个子组件方法
};
</script>
<!-- 子组件 ChildComponent.vue -->
<template>
<input ref="inputRef" />
</template>
<script setup>
import { ref } from 'vue';
const inputRef = ref(null);
// 定义暴露给父组件的方法
const focusInput = () => {
inputRef.value.focus();
};
const resetForm = () => {
// 重置表单逻辑
};
// 暴露方法
defineExpose({
focusInput,
resetForm
});
</script>
特点:
- 父组件可以直接调用子组件方法
- 适合在父组件触发子组件动作(如聚焦输入框)
- 需要显式暴露组件内部方法
最佳实践:
- 仅暴露必要的方法和属性
- 避免暴露过多组件内部状态
- 结合 Typescript 提供类型提示
ts
// 子组件定义暴露类型
defineExpose({
focusInput: () => void,
resetForm: () => void
});
// 父组件类型声明
import type { Ref } from 'vue';
import type { ChildComponentType } from './ChildComponent.vue';
const childRef: Ref<ChildComponentType | null> = ref(null);
5. $attrs(属性透传)
原理 :$attrs
包含父组件传递的、但未被子组件声明为 props 的所有属性,用于实现属性透传
核心特征:
- 自动透传 :未声明为 props 的属性会自动附加到组件的根元素
- 透传控制 :使用
inheritAttrs: false
可禁用自动透传 - 分层透传 :可在组件中手动将
$attrs
传递给深层子组件 - Vue3 变化:不再包含 class/style 属性
基础使用示例
html
<!-- 父组件 -->
<ChildComponent
title="属性透传示例"
data-id="123"
class="custom-class"
@custom-event="handleEvent"
/>
<!-- 子组件 ChildComponent.vue -->
<template>
<!-- 单个根元素会自动获得透传属性 -->
<div>组件内容</div>
<!--
渲染结果:
<div
data-id="123"
onCustom-event="handleEvent"
class="custom-class"
>
组件内容
</div>
-->
</template>
<script setup>
defineProps({
title: String // 声明title prop,$attrs中不包含title
});
</script>
手动控制透传
html
<!-- 禁用自动透传 -->
<script>
defineOptions({
inheritAttrs: false
})
</script>
<template>
<div class="wrapper">
<!-- 手动绑定到指定元素 -->
<main-content v-bind="$attrs" />
<!-- 只绑定特定属性 -->
<div :data-id="$attrs['data-id']"></div>
<!-- 过滤掉某些属性 -->
<footer v-bind="filteredAttrs"></footer>
</div>
</template>
<script setup>
import { computed, useAttrs } from 'vue';
const attrs = useAttrs();
// 过滤掉事件监听器
const filteredAttrs = computed(() => {
const { onCustomEvent, ...rest } = attrs;
return rest;
});
</script>
多层透传模式
html
<!-- 父组件 -->
<ContainerComponent
id="main-container"
data-tracking="true"
/>
<!-- ContainerComponent.vue -->
<template>
<div class="container">
<ContentWrapper v-bind="$attrs" />
</div>
</template>
<!-- ContentWrapper.vue -->
<template>
<section class="content">
<ActualContent v-bind="$attrs" />
</section>
</template>
<!-- ActualContent.vue -->
<template>
<article v-bind="$attrs">
实际内容
</article>
</template>
<!-- 最终渲染结果 -->
<article
id="main-container"
data-tracking="true"
>
实际内容
</article>
最佳实践:
- 基础组件必用
按钮、输入框等基础组件都应支持属性透传:
html
<!-- BaseButton.vue -->
<template>
<button v-bind="$attrs">
<slot />
</button>
</template>
<script setup>
defineOptions({ inheritAttrs: false })
</script>
- 智能属性分配
将不同属性分发给对应子元素:
html
<template>
<div>
<input :placeholder="$attrs.placeholder">
<button v-bind="buttonAttrs"><slot></slot></button>
</div>
</template>
<script setup>
const buttonAttrs = computed(() => ({
class: $attrs.class,
style: $attrs.style,
disabled: $attrs.disabled
}))
</script>
- 事件处理策略
合并透传事件和组件事件:
html
<template>
<div @click="handleClick">
<!-- ... -->
</div>
</template>
<script setup>
const emit = defineEmits(['click'])
const attrs = useAttrs()
const handleClick = (e) => {
emit('click', e) // 触发组件事件
attrs.onClick?.(e) // 触发透传事件
}
</script>
- TS类型定义
增强类型安全性:
ts
import type { Attrs } from 'vue'
// 声明自定义属性类型
interface CustomAttrs extends Attrs {
'data-tracking'?: string
'aria-role'?: string
}
// 使用类型断言
const attrs = useAttrs() as CustomAttrs
6. Pinia(状态管理)
原理:官方推荐的状态管理库,集中管理全局应用状态
基础使用
js
// store/counter.js
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++;
},
reset() {
this.count = 0;
}
},
getters: {
doubleCount: (state) => state.count * 2
}
});
- state :存储状态数据(类似于组件的
data
),可以通过store.count
访问 - actions :修改 state 的方法(类似于组件的
methods
),可以直接调用这些方法,例如:store.increment()
- getters :计算属性(类似于组件的
computed
),可以像访问 state 一样访问 getters,例如:store.doubleCount
组件中使用
html
<template>
<div>当前计数: {{ count }}</div>
<div>双倍计数: {{ doubleCount }}</div>
<button @click="increment">增加</button>
<button @click="reset">重置</button>
</template>
<script setup>
import { useCounterStore } from '@/store/counter';
const counter = useCounterStore();
// 将 store 的 state 和 getters 转换为 ref 对象
const { count, doubleCount } = storeToRefs(counter);
const { increment, reset } = counter;
</script>
特点:
- 类型安全(TypeScript友好)
- 响应式API
- 模块化设计
- 支持SSR
- 开发者工具集成
最佳实践:
- 按功能组织store模块
- 避免直接修改状态,使用actions
- 使用 storeToRefs 保持响应式
- 在setup()函数之外使用需谨慎
- 组合store:
js
// store/user.js
export const useUserStore = defineStore('user', {
state: () => ({
name: 'Guest'
})
});
// store/combined.js
import { useCounterStore } from './counter';
import { useUserStore } from './user';
export const useCombinedStore = defineStore('combined', () => {
const counterStore = useCounterStore();
const userStore = useUserStore();
const fullInfo = computed(() => `${userStore.name}: ${counterStore.count}`);
return { fullInfo };
});
7. 事件总线(Mitt)
原理:使用轻量级的事件库实现跨组件通信
bash
npm install mitt
js
// eventBus.js
import mitt from 'mitt';
export default mitt();
// 组件A(发送消息)
import bus from './eventBus';
bus.emit('user-login', { username: 'john', time: new Date() });
// 组件B(监听消息)
import bus from './eventBus';
onMounted(() => {
bus.on('user-login', (userData) => {
console.log('用户登录:', userData);
});
});
// 组件卸载时移除监听
onUnmounted(() => {
bus.off('user-login');
});
特点:
- 简单轻量(约200字节)
- 无Vue实例依赖
- 适用于没有直接关系的组件
- 支持多种事件类型(on, off, emit等)
最佳实践:
- 避免全局滥用,控制在特定模块内
- 推荐使用命名空间
js
// 使用命名空间
const events = {
USER_LOGIN: 'auth/login',
USER_LOGOUT: 'auth/logout',
// ...
};
- 封装为可复用的hook
js
// useEventBus.js
import { onUnmounted } from 'vue';
import bus from './eventBus';
export function useEventBus(eventName, callback) {
bus.on(eventName, callback);
// 自动清理
onUnmounted(() => {
bus.off(eventName, callback);
});
}
Reactive 状态提升
原理:将共享状态提升到公共祖先组件
html
<!-- 祖先组件 -->
<script setup>
import { reactive } from 'vue';
import ChildA from './ChildA.vue';
import ChildB from './ChildB.vue';
const sharedState = reactive({
count: 0,
theme: 'light'
});
const updateTheme = (newTheme) => {
sharedState.theme = newTheme;
};
</script>
<template>
<ChildA :shared="sharedState" @change-theme="updateTheme" />
<ChildB :shared="sharedState" />
</template>
特点:
- 当多个同级组件需要共享状态时使用
- 适用于紧密相关的组件
- 避免状态在多个地方重复定义
综合最佳实践建议
-
简单优先原则:
- 优先使用 Props/Emits
- 表单组件优先使用 v-model
- 兄弟组件通信考虑状态提升
-
状态管理:
- 中大型项目使用 Pinia 管理全局状态
- 避免直接在组件中修改 store 状态
-
跨层级通信:
- 深层嵌套场景用 Provide/Inject
- 配合 Symbol 避免命名冲突
-
方法调用:
- 父调用子使用 ref + defineExpose
- 其他关系方法调用优先通过 store action
-
性能优化:
- 避免 Props 传递大型对象(使用引用传递时谨慎)
- 使用计算属性优化渲染性能
- Provide 提供的响应式数据使用 shallowRef/shallowReactive 优化深层嵌套对象
-
可维护性:
- 使用 TypeScript 增强类型安全
- 模块化组织通信代码
- 统一规范通信接口类型