VUE3的组件通信,相信你看完会有更清楚的理解!

如有不对的或者遗漏的,欢迎指正!希望大家一起共同进步,共勉!

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 的所有属性,用于实现属性透传

核心特征:

  1. 自动透传 :未声明为 props 的属性会自动附加到组件的根元素
  2. 透传控制 :使用 inheritAttrs: false 可禁用自动透传
  3. 分层透传 :可在组件中手动将 $attrs 传递给深层子组件
  4. 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>

最佳实践

  1. 基础组件必用
    按钮、输入框等基础组件都应支持属性透传:
html 复制代码
<!-- BaseButton.vue -->
<template>
  <button v-bind="$attrs">
    <slot />
  </button>
</template>
<script setup>
defineOptions({ inheritAttrs: false })
</script>
  1. 智能属性分配
    将不同属性分发给对应子元素:
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>
  1. 事件处理策略
    合并透传事件和组件事件:
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>
  1. 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>

特点

  • 当多个同级组件需要共享状态时使用
  • 适用于紧密相关的组件
  • 避免状态在多个地方重复定义

综合最佳实践建议

  1. 简单优先原则

    • 优先使用 Props/Emits
    • 表单组件优先使用 v-model
    • 兄弟组件通信考虑状态提升
  2. 状态管理

    • 中大型项目使用 Pinia 管理全局状态
    • 避免直接在组件中修改 store 状态
  3. 跨层级通信

    • 深层嵌套场景用 Provide/Inject
    • 配合 Symbol 避免命名冲突
  4. 方法调用

    • 父调用子使用 ref + defineExpose
    • 其他关系方法调用优先通过 store action
  5. 性能优化

    • 避免 Props 传递大型对象(使用引用传递时谨慎)
    • 使用计算属性优化渲染性能
    • Provide 提供的响应式数据使用 shallowRef/shallowReactive 优化深层嵌套对象
  6. 可维护性

    • 使用 TypeScript 增强类型安全
    • 模块化组织通信代码
    • 统一规范通信接口类型
相关推荐
逝缘~9 分钟前
小白学Pinia状态管理
前端·javascript·vue.js·vscode·es6·pinia
光影少年12 分钟前
vite原理
前端·javascript·vue.js
源猿人40 分钟前
文化与代码的交汇:OpenCC 驱动的中文语系兼容性解决方案
前端·vue.js
難釋懷1 小时前
Vue非单文件组件
前端·vue.js
克里斯前端1 小时前
vue在打包的时候能不能固定assets里的js和css文件名称
javascript·css·vue.js
OpenTiny社区2 小时前
HDC2025即将拉开序幕!OpenTiny重新定义前端智能化解决方案~
前端·vue.js·github
zhoxier2 小时前
element-ui 的el-table,多选翻页后,之前选择的数据丢失问题处理
vue.js·ui·elementui
栀一一2 小时前
@click和@click.stop的区别
vue.js
Midsummer2 小时前
nuxt安装报错-网络问题
vue.js·nuxt.js
张童瑶2 小时前
Vue Electron 使用来给若依系统打包成exe程序,出现登录成功但是不跳转页面(已解决)
javascript·vue.js·electron