引言

在 Vue.js 的组件化开发中,组件间的通信是构建复杂应用的核心。Vue3 相比 Vue2 在组件通信方面有了显著的变化和改进,这些改进不仅简化了开发流程,还提供了更强大、更灵活的数据传递方式。本文将深入探讨 Vue3 中各种组件通信方式,并结合实际代码示例进行详细说明。

Vue3 与 Vue2 组件通信的主要区别

在开始具体通信方式之前,让我们先了解 Vue3 相对于 Vue2 在组件通信方面的主要变化:

  1. 移除事件总线,使用 mitt 代替:Vue3 移除了内置的事件总线系统,推荐使用第三方库 mitt 来实现任意组件间的通信
  2. Vuex 换成了 Pinia:状态管理库从 Vuex 迁移到更轻量、类型安全的 Pinia
  3. .sync 修饰符优化到 v-model:Vue3 将 .sync 修饰符的功能整合到了 v-model 中,使双向绑定更加统一
  4. listeners合并到listeners 合并到 attrs:简化了属性传递机制
  5. 移除了 $children:简化组件实例访问方式

这些变化使 Vue3 的组件通信更加简洁、一致,同时保持了强大的功能。

1. Props:父子组件的基础通信方式

1.1 基本概念

Props 是 Vue 中最基础的组件通信方式,用于父组件向子组件传递数据。

关键点

  • 父传子:属性是非函数
  • 子传父:属性是函数(通过回调函数实现)

1.2 示例分析

vue

复制下载

<!-- 父组件 Father.vue -->
<template>
  <div class="father">
    <h3>父组件</h3>
    <h4>汽车:{{ car }}</h4>
    <h4 v-show="toy">父接收到子的玩具:{{ toy }}</h4>
    <!-- 传递数据和回调函数给子组件 -->
    <Child :car="car" :sendToy="getTony"/>
  </div>
</template>

<script setup lang="ts" name="Father">
import {ref} from 'vue';
import Child from './Child.vue';

let car = ref('奔驰');
let toy = ref('');
function getTony(value:string){
  console.log('父接收到子的玩具',value);
  toy.value = value;
}
</script>

vue

复制下载

<!-- 子组件 Child.vue -->
<template>
  <div class="child">
    <h3>子组件</h3>
    <h4>玩具:{{ toy }}</h4>
    <h4>父给子的汽车:{{ car }}</h4>
    <!-- 通过调用父组件传递的函数实现子传父 -->
    <button @click="sendToy(toy)">点击子给父传递玩具</button>
  </div>
</template>

<script setup lang="ts" name="Child">
import {ref} from 'vue';
let toy = ref('奥特曼');
// 接收父组件传递的props
defineProps(['car','sendToy']);
</script>

代码解析

  1. 父组件通过 :car="car" 将数据传递给子组件
  2. 父组件通过 :sendToy="getTony" 将回调函数传递给子组件
  3. 子组件通过 defineProps 接收 props
  4. 子组件通过调用 sendToy(toy) 将数据传回父组件

2. 自定义事件:子组件向父组件通信

2.1 原生事件 vs 自定义事件

特性原生事件自定义事件
事件名特定的(click、mouseenter等)任意名称
事件对象包含事件相关信息的对象调用 emit 时传递的数据
命名规范-建议使用烤肉串写法(如 send-toy)

2.2 示例实现

vue

复制下载

<!-- 子组件 Child.vue -->
<template>
  <div class="child">
    <h3>子组件</h3>
    <h4>玩具:{{ toy }}</h4>
    <button @click="sendToy">传递玩具给父</button>
  </div>
</template>

<script setup lang="ts" name="Child">
import {ref} from 'vue';
let toy = ref('小汽车');

// 定义自定义事件
const emit = defineEmits(['sendToy']);
function sendToy(){
  // 触发自定义事件,并传递参数
  emit('sendToy', toy.value);
}
</script>

vue

复制下载

<!-- 父组件 Father.vue -->
<template>
  <div class="father">
    <h3>父组件</h3>
    <h4 v-show="toy">收到的玩具:{{ toy }}</h4>
    <!-- 子组件的自定义事件 -->
    <Child @sendToy="saveToy"/>
  </div>
</template>

<script setup lang="ts" name="Father">
import Child from './Child.vue';
import { ref } from 'vue';

let toy = ref('');
function saveToy(value:string){
  toy.value = value;
  console.log('父组件接收到子组件传递的玩具:', toy.value);
}
</script>

3. Mitt:任意组件间的事件通信

3.1 Mitt 简介

Mitt 是一个小巧的发布-订阅库,用于在 Vue3 中实现任意组件间的通信。

核心 API

  • on:事件
  • off:移除事件
  • emit:触发事件
  • all.clear:清除所有事件

3.2 示例实现

typescript

复制下载

// emitter.ts
import mitt from 'mitt';
const emitter = mitt();
export default emitter;

vue

复制下载

<!-- 发送组件 Child1.vue -->
<template>
  <div class="child1">
    <h3>子组件1</h3>
    <h4>玩具:{{ toy }}</h4>
    <button @click="emitter.emit('send-toy',toy)">玩具给Child2</button>
  </div>
</template>

<script setup lang="ts" name="Child1">
import {ref} from "vue";
import emitter from '@/utils/emitter';

let toy= ref("奥特曼");
</script>

vue

复制下载

<!-- 接收组件 Child2.vue -->
<template>
  <div class="child2">
    <h3>子组件2</h3>
    <h4>电脑:{{ computer }}</h4>
    <h4 v-show="toy">玩具:{{ toy }}</h4>
  </div>
</template>

<script setup lang="ts" name="Child2">
import {ref,onUnmounted} from "vue";
import emitter from '@/utils/emitter';

let computer = ref("联想");
let toy = ref("");  
// 事件
emitter.on('send-toy',(value : string)=>{
  console.log('sent-toy');
  toy.value = value ;
})

// 组件卸载时移除事件
onUnmounted(()=>{
  emitter.off('send-toy');
}) 
</script>

重要提示:使用 mitt 时,一定要记得在组件卸载时移除事件,避免内存泄漏。

4. v-model:双向绑定的升级版

4.1 v-model 的本质

在 Vue3 中,组件上的 v-model 本质上是 :modelValue 和 @update:modelValue 的语法糖。

4.2 基本使用示例

vue

复制下载

<!-- 父组件 Father.vue -->
<template>
  <div class="father">
    <h3>父组件</h3>
    <!-- 使用 v-model 进行双向绑定 -->
    <Test v-model="username"/>
  </div>
</template>

<script setup lang="ts" name="Father">
import{ref} from "vue";
import Test from './Test.vue';
let username = ref("张三");
</script>

vue

复制下载

<!-- 子组件 Test.vue -->
<template>
  <div>
    <input type="text" 
      :value="modelValue"
      @input="emit('update:modelValue',(<HTMLInputElement>$event.target).value)"
    >
  </div>
</template>

<script setup lang="ts" name="Test">
defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
</script>

4.3 多个 v-model 绑定

Vue3 支持在单个组件上使用多个 v-model:

vue

复制下载

<AtguiguInput v-model:abc="userName" v-model:xyz="password"/>

实现原理

  • v-model:abc 对应 :abc 和 @update:abc
  • v-model:xyz 对应 :xyz 和 @update:xyz

5. $attrs:祖孙组件通信的桥梁

5.1 $attrs 的作用

$attrs 用于实现父组件向孙组件通信,它包含了父组件传入的所有非 prop 属性。

5.2 示例实现

vue

复制下载

<!-- 祖组件 Father.vue -->
<template>
  <div class="father">
    <h3>父组件</h3>
    <h4>a:{{ a }}</h4>
    <h4>b:{{ b }}</h4>
    <h4>c:{{ c }}</h4>
    <h4>d:{{ d }}</h4>
    <!-- 传递多个属性和方法 -->
    <Child
      :a="a"
      :b="b"
      :c="c"
      :d="d"
      v-bind="{ x: 100, y: 200 }"
      :updateA="updateA"
    />
  </div>
</template>

vue

复制下载

<!-- 子组件 Child.vue -->
<template>
  <div class="child">
    <h3>子组件</h3>
    <!-- 使用 v-bind="$attrs" 将属性传递给孙组件 -->
    <GrandChild v-bind="$attrs" />
  </div>
</template>

vue

复制下载

<!-- 孙组件 GrandChild.vue -->
<template>
  <div class="grand-child">
    <h3>孙组件</h3>
    <h4>a:{{ a }}</h4>
    <h4>b:{{ b }}</h4>
    <h4>c:{{ c }}</h4>
    <h4>d:{{ d }}</h4>
    <h4>x:{{ x }}</h4>
    <h4>y:{{ y }}</h4>
    <!-- 调用祖组件传递的方法 -->
    <button @click="updateA(6)">点我将爷爷那的a更新</button>
  </div>
</template>

<script setup lang="ts" name="GrandChild">
// 接收所有从祖组件传递过来的属性
defineProps(['a', 'b', 'c', 'd', 'x', 'y', 'updateA'])
</script>

注意$attrs 会自动排除在 props 中声明的属性,这些属性被子组件自己"消费"了。

6. refsrefs 和 parent:组件实例的直接访问

6.1 基本概念

  • $refs:用于父组件访问子组件实例(父→子)
  • $parent:用于子组件访问父组件实例(子→父)

6.2 示例实现

vue

复制下载

<!-- 父组件 Father.vue -->
<template>
  <div class="father">
    <h3>父组件</h3>
    <h4>房产:{{ house }}</h4>
    <button @click="changeToy">修改Child1的玩具</button>
    <button @click="changeComputer">修改Child2的电脑</button>
    <button @click="getAllChild($refs)">获取所有的子组件实列对象</button>
    <!-- 使用 ref 标识子组件 -->
    <Child1 ref="c1"/>
    <Child2 ref="c2"/>
  </div>
</template>

<script setup lang="ts" name="Father">
import Child1 from './Child1.vue';
import Child2 from './Child2.vue';
import {ref} from 'vue';

let house = ref(4);

let c1 = ref();
let c2 = ref();

function changeToy(){
  // 通过 ref 访问子组件实例并修改其数据
  c1.value.toy = '小猪佩奇'
}

function changeComputer(){
  c2.value.computer = '戴尔'
}

function getAllChild(refs:any){
  console.log(refs);
  for(let key in refs){
    refs[key].book += 2;
  }   
}

// 将数据暴露给子组件
defineExpose({house});
</script>

vue

复制下载

<!-- 子组件 Child1.vue -->
<template>
  <div class="child1">
    <h3>Child1</h3>
    <h4>玩具:{{ toy }}</h4>
    <h4>书籍:{{ book }}</h4>
    <!-- 通过 $parent 访问父组件实例 -->
    <button @click="minusHouse($parent)">干掉父亲的一套房产</button>
  </div>
</template>

<script setup lang="ts" name="Child1">
import {ref} from 'vue';
let toy = ref('奥特曼');
let book = ref(3);

function minusHouse(parent : any){
  console.log(parent);
  // 修改父组件的数据
  parent.house -=1;
}

// 将数据暴露给父组件
defineExpose({toy, book});
</script>

重要提示:使用 defineExpose 宏函数将数据暴露给外部组件使用。

7. Provide 和 Inject:依赖注入

7.1 基本概念

Provide 和 Inject 用于实现祖孙组件间的直接通信,无需经过中间组件。

7.2 示例实现

vue

复制下载

<!-- 祖组件 Father.vue -->
<template>
  <div class="father">
    <h3>父组件</h3>
    <h4>银子:{{ money }}</h4>
    <h4>车子:一辆{{ car.brand }}车, {{ car.price }}万元</h4>
    <Child/>
  </div>
</template>

<script setup lang="ts" name="Father">
import Child from './Child.vue'
import { ref , reactive ,provide} from 'vue'

let money = ref(1000)
function changeMoney(value:number){
  money.value -= value;
}

let car = reactive({
  brand:'奔驰',
  price:100
})

// 向后代提供数据
provide('moneyContext',{money,changeMoney});
provide('che',car);
</script>

vue

复制下载

<!-- 孙组件 GrandChild.vue -->
<template>
  <div class="grand-child">
    <h3>孙组件</h3>
    <h4>银子:{{ money }}</h4>
    <h4>车子:一辆{{ car.brand }}车, {{ car.price }}万元</h4>
    <!-- 调用祖组件提供的方法 -->
    <button @click="changeMoney(100)">花爷爷的银子</button>
  </div>
</template>

<script setup lang="ts" name="GrandChild">
import { inject } from 'vue'

// 注入祖组件提供的数据和方法
let {money,changeMoney} = inject('moneyContext',{money:null,changeMoney:(value:number)=>{}});
let car = inject('che',{brand:'',price:0});
</script>

关键点

  1. 使用 provide 在祖先组件中提供数据
  2. 使用 inject 在后代组件中注入数据
  3. 可以传递响应式数据和函数

8. 插槽:内容分发的艺术

8.1 默认插槽

vue

复制下载

<!-- 子组件 Category.vue -->
<template>
  <div class="category"> 
    <h2>{{title}}</h2>
    <!-- 默认插槽 -->
    <slot>默认内容</slot>
  </div>
</template>

vue

复制下载

<!-- 父组件 Father.vue -->
<template>
  <div>
    <h3>父组件</h3>
    <div class="content"> 
      <Category title="游戏列表">
        <!-- 插入内容到默认插槽 -->
        <ul>
          <li v-for="g in games" :key="g.id">{{g.name}}</li>
        </ul>  
      </Category>
    </div>
  </div>
</template>

8.2 具名插槽

vue

复制下载

<!-- 子组件 Category.vue -->
<template>
  <div class="category"> 
    <!-- 具名插槽 -->
    <slot name="s1">默认内容1</slot>
    <slot name="s2">默认内容2</slot>
  </div>
</template>

vue

复制下载

<!-- 父组件 Father.vue -->
<template>
  <div>
    <h3>父组件</h3>
    <div class="content"> 
      <Category>
        <!-- 使用 v-slot 指令指定插槽名称 -->
        <template v-slot:s1>
          <h2>游戏列表</h2>
        </template>
        <template v-slot:s2>
           <ul>
            <li v-for="g in games" :key="g.id">{{g.name}}</li>
          </ul> 
        </template>
      </Category>
    </div>
  </div>
</template>

8.3 作用域插槽

vue

复制下载

<!-- 子组件 Game.vue -->
<template>
  <div class="game"> 
    <slot name="s1"></slot>
    <!-- 作用域插槽,传递数据给父组件 -->
    <slot name="s2" :games="games"></slot>
  </div>
</template>

vue

复制下载

<!-- 父组件 Father.vue -->
<template>
  <div>
    <h3>父组件</h3>
    <div class="content">
      <Game>
        <template v-slot:s1>
          <h2>游戏列表1</h2>
        </template>
        <!-- 接收子组件传递的数据 -->
        <template v-slot:s2="{ games }">
          <ul>
            <li v-for="g in games" :key="g.id">{{g.name}}</li>
          </ul>
        </template>
      </Game>
    </div> 
  </div>
</template>

作用域插槽的核心思想:数据在子组件中,但数据的渲染结构由父组件决定。

9. Pinia:现代化的状态管理

虽然本文主要代码示例中没有包含 Pinia 的具体实现,但作为 Vue3 推荐的状态管理库,它在复杂应用中的组件通信中扮演着重要角色。

Pinia 的优势

  1. 类型安全:完整的 TypeScript 支持
  2. 模块化:每个 store 都是独立的模块
  3. 轻量级:相比 Vuex 更简洁
  4. 组合式 API:与 Vue3 的组合式 API 完美结合

总结:Vue3 组件通信的最佳实践

通过以上九种通信方式的详细分析,我们可以总结出 Vue3 组件通信的最佳实践:

通信方式选择指南

通信场景推荐方式说明
父子组件通信Props / 自定义事件简单直接,符合单向数据流
子父组件通信自定义事件通过事件传递数据
双向绑定v-model简洁高效
任意组件通信Mitt / Pinia根据复杂度选择
祖孙组件通信Provide/Inject / $attrs避免 prop 逐层传递
组件实例访问refs/refs / parent谨慎使用,破坏封装性
内容分发插槽灵活的内容分发机制

性能优化建议

  1. 避免过度使用 refsrefs 和 parent:这些方式会破坏组件的封装性,增加耦合度
  2. 合理使用 Provide/Inject:对于深层嵌套的组件通信非常高效
  3. 及时清理事件:使用 mitt 时,记得在组件卸载时移除
  4. 合理分割状态:使用 Pinia 管理全局状态,避免 props 传递过深

代码可维护性建议

  1. 保持单向数据流:尽量使用 props 向下传递,事件向上传递
  2. 明确的接口定义:使用 TypeScript 明确 props 和事件类型
  3. 适度的组件拆分:避免组件过于复杂,合理拆分提高可维护性
  4. 统一的命名规范:事件名使用烤肉串写法,props 使用驼峰式

结语

Vue3 的组件通信机制在继承 Vue2 优秀设计的基础上,进行了许多优化和改进。从简单的 props 到复杂的状态管理,Vue3 提供了丰富的工具来满足不同场景下的通信需求。理解并合理运用这些通信方式,将帮助开发者构建更加健壮、可维护的 Vue3 应用。

在实际开发中,建议根据具体场景选择合适的通信方式,遵循 Vue 的设计哲学,保持组件的独立性和可复用性。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com