Vue2 vs Vue3:组件通信方式全解析与深度对比

组件化是 Vue 框架的核心灵魂,而组件通信则是组件化开发中最基础、最高频的能力。从 Vue2 到 Vue3 的演进中,组件通信的实现方式发生了诸多变化:部分 API 被废弃,部分能力被优化整合,同时也诞生了更符合现代工程化的新方案。

本文将全面梳理 Vue2 与 Vue3 中所有主流的组件通信方式,结合代码示例讲解用法,并从设计理念、适用场景、开发体验等维度做深度对比,帮你在不同项目场景下选择最合适的通信方案。

一、Vue2 组件通信方式全览

Vue2 基于选项式 API(Options API)设计,提供了丰富且灵活的通信手段,覆盖父子、隔代、兄弟、全局等所有场景。

1. 基础父子通信:props / $emit

这是 Vue 最经典、最符合单向数据流设计的通信方式,也是日常开发中使用频率最高的方案。

  • 父传子 :父组件通过自定义属性向下传递数据,子组件用 props 声明接收
  • 子传父 :子组件通过 $emit 触发自定义事件,父组件监听事件并接收参数

代码示例

vue 复制代码
<!-- 父组件 Parent.vue -->
<template>
  <Child 
    :message="parentMsg" 
    @update-text="handleUpdate"
  />
</template>
<script>
import Child from './Child.vue'
export default {
  components: { Child },
  data() {
    return { parentMsg: '来自父组件的数据' }
  },
  methods: {
    handleUpdate(val) {
      console.log('子组件传来的值:', val)
    }
  }
}
</script>
vue 复制代码
<!-- 子组件 Child.vue -->
<template>
  <div @click="sendToParent">{{ message }}</div>
</template>
<script>
export default {
  props: {
    message: {
      type: String,
      default: ''
    }
  },
  methods: {
    sendToParent() {
      this.$emit('update-text', '子组件的回传数据')
    }
  }
}
</script>

补充:Vue2 还提供了 .sync 修饰符,是 v-model 的语法糖扩展,用于简化"子组件修改父组件值"的写法,在 Vue3 中被多 v-model 替代。

2. 父子实例直接访问:parent / children

允许父子组件直接访问对方的实例对象,从而读取数据、调用方法,使用非常便捷,但破坏了组件封装性。

  • this.$parent:子组件获取父组件实例
  • this.$children:父组件获取子组件实例数组

注意事项

  • $children 不保证顺序,也不是响应式的
  • 深度依赖会导致组件耦合度高,难以维护和复用

3. 隔代属性透传:attrs / listeners

Vue 2.4 版本引入,专门用于解决多层级组件属性/事件透传问题,避免 props 层层传递,非常适合封装高阶组件、二次封装 UI 组件库。

  • $attrs:包含父作用域中不被 props 识别的特性(class 和 style 除外)
  • $listeners:包含父作用域中所有的 v-on 事件监听器

典型场景:爷组件 → 父组件(包装层)→ 孙组件,父组件只做透传,不关心具体属性。

vue 复制代码
<!-- 父组件(中间层)Father.vue -->
<template>
  <!-- v-bind="$attrs" 透传属性,v-on="$listeners" 透传事件 -->
  <GrandChild v-bind="$attrs" v-on="$listeners" />
</template>

4. 跨层级依赖注入:provide / inject

Vue 2.2.0 新增的 API,用于祖先组件向所有后代组件注入数据,无论层级多深都可以直接获取,完美解决深层级 props 透传的痛点。

  • provide:祖先组件中定义要提供的数据/方法
  • inject:后代组件中声明要注入的数据

代码示例

javascript 复制代码
// 爷组件 GrandFather.vue
export default {
  provide() {
    return {
      theme: 'dark',
      changeTheme: this.changeTheme
    }
  },
  methods: {
    changeTheme(theme) {
      this.theme = theme
    }
  }
}
javascript 复制代码
// 孙组件 GrandSon.vue
export default {
  inject: ['theme', 'changeTheme'],
  mounted() {
    console.log(this.theme) // 'dark'
  }
}

重要注意:Vue2 中 provide/inject 默认不是响应式的。只有当传入的是一个响应式对象时,对象内部的属性变化才会触发视图更新。

5. 组件实例引用:ref / $refs

给子组件或 DOM 元素添加 ref 属性,父组件通过 this.$refs.xxx 直接获取其实例,从而调用子组件的方法、访问子组件的数据。

vue 复制代码
<template>
  <Child ref="childComp" />
</template>
<script>
export default {
  mounted() {
    // 直接调用子组件的方法
    this.$refs.childComp.resetForm()
  }
}
</script>

注意:$refs 只在组件渲染完成后才填充,且不是响应式的,避免在模板或计算属性中使用。

6. 任意组件通信:EventBus(事件总线)

通过一个空的 Vue 实例作为中央事件总线,所有组件都可以通过它来触发和监听事件,实现任意组件间的通信(兄弟、跨级、无关联组件)。

代码示例

javascript 复制代码
// eventBus.js
import Vue from 'vue'
export const EventBus = new Vue()
javascript 复制代码
// 组件A:触发事件
import { EventBus } from './eventBus.js'
EventBus.$emit('sendMsg', '来自A组件的消息')
javascript 复制代码
// 组件B:监听事件
import { EventBus } from './eventBus.js'
mounted() {
  EventBus.$on('sendMsg', (val) => {
    console.log(val)
  })
},
beforeDestroy() {
  // 必须手动销毁,防止内存泄漏
  EventBus.$off('sendMsg')
}

局限性:只适合小型项目。大型项目中事件分散、难以追踪,容易导致逻辑混乱和内存泄漏。

7. 全局状态管理:Vuex

Vue 官方的集中式状态管理方案,将所有组件的共享状态抽取到一个全局 store 中统一管理,通过约定的规则修改状态,保证状态变化可追踪。

核心概念:

  • state:全局状态数据
  • mutations:唯一修改 state 的方式,同步操作
  • actions:处理异步逻辑,提交 mutation
  • getters:state 的计算属性
  • modules:模块化拆分 store

适合中大型项目,存在大量跨组件共享状态的场景。

二、Vue3 组件通信方式全览

Vue3 基于组合式 API(Composition API)重构,同时推出了 <script setup> 语法糖,通信方式在继承核心思想的基础上,做了大量优化和精简,更强调封装性和规范性。

1. 基础父子通信:defineProps / defineEmits

核心思想依然是「props 向下,事件向上」,但在 <script setup> 中通过编译器宏实现,写法更简洁。

  • defineProps:声明接收的 props,返回 props 对象
  • defineEmits:声明可触发的事件,返回 emit 函数

代码示例

vue 复制代码
<!-- 父组件 Parent.vue -->
<template>
  <Child 
    :count="num" 
    @update-count="handleUpdate"
    v-model:title="pageTitle"
  />
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const num = ref(10)
const pageTitle = ref('首页')

const handleUpdate = (val) => {
  num.value = val
}
</script>
vue 复制代码
<!-- 子组件 Child.vue -->
<template>
  <div>{{ count }} - {{ title }}</div>
  <button @click="changeCount">修改</button>
</template>
<script setup>
const props = defineProps({
  count: {
    type: Number,
    default: 0
  },
  title: String
})

const emit = defineEmits(['update-count', 'update:title'])

const changeCount = () => {
  emit('update-count', props.count + 1)
  emit('update:title', '新标题')
}
</script>

重要变化:Vue3 中 v-model 全面升级,默认属性为 modelValue,默认事件为 update:modelValue,且支持一个组件上绑定多个 v-model ,正式替代了 Vue2 的 .sync 修饰符。

2. 隔代属性透传:$attrs

Vue3 做了大幅简化:移除了 $listeners ,将所有事件监听器统一合并到 $attrs 中。

现在 $attrs 包含了所有未被 props 声明的属性、class、style 以及事件监听器,透传只需要写 v-bind="$attrs" 即可。

vue 复制代码
<!-- 中间层组件 Father.vue -->
<template>
  <!-- 一次性透传所有属性和事件 -->
  <GrandChild v-bind="$attrs" />
</template>

3. 跨层级依赖注入:provide / inject

Vue3 的组合式 API 对 provide/inject 做了全面增强,是深层级通信的首选方案。

  • 支持传递响应式数据(ref / reactive
  • 提供 readonly 保护数据,防止子组件意外修改父级状态
  • 支持传递函数,实现子组件调用祖先组件的方法

代码示例

vue 复制代码
<!-- 祖先组件 Ancestor.vue -->
<script setup>
import { ref, provide, readonly } from 'vue'

const theme = ref('dark')
const changeTheme = (newTheme) => {
  theme.value = newTheme
}

// 用 readonly 保护数据,子组件只能读取不能直接修改
provide('theme', readonly(theme))
provide('changeTheme', changeTheme)
</script>
vue 复制代码
<!-- 后代组件 Descendant.vue -->
<script setup>
import { inject } from 'vue'

const theme = inject('theme', 'light') // 第二个参数是默认值
const changeTheme = inject('changeTheme')
</script>

4. 组件实例引用:ref + defineExpose

Vue3 对组件封装做了强化:使用 <script setup> 的组件默认是封闭的 ,外部通过 ref 无法直接访问组件内部的属性和方法。

必须使用 defineExpose 显式暴露需要对外公开的内容。

代码示例

vue 复制代码
<!-- 子组件 Child.vue -->
<script setup>
import { ref } from 'vue'

const formData = ref({ name: '' })
const resetForm = () => {
  formData.value = { name: '' }
}

// 显式暴露给父组件
defineExpose({
  resetForm
})
</script>
vue 复制代码
<!-- 父组件 Parent.vue -->
<template>
  <Child ref="childRef" />
  <button @click="handleReset">重置表单</button>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const childRef = ref(null)

const handleReset = () => {
  childRef.value.resetForm()
}
</script>

补充:Vue3 正式移除了 $children$parent 虽然还存在,但官方不推荐使用,更推崇单向数据流和显式暴露的规范。

5. 任意组件通信:mitt / tiny-emitter

Vue3 移除了 Vue 实例上的 $on$off$once 等事件方法,因此不再支持原生的 EventBus 写法。

官方推荐使用 mitttiny-emitter 等轻量级第三方事件库来实现事件总线功能。

代码示例(mitt)

bash 复制代码
npm install mitt
javascript 复制代码
// eventBus.js
import mitt from 'mitt'
export const emitter = mitt()
vue 复制代码
<!-- 组件A:触发事件 -->
<script setup>
import { emitter } from './eventBus.js'
emitter.emit('sendMsg', '来自A的消息')
</script>
vue 复制代码
<!-- 组件B:监听事件 -->
<script setup>
import { onUnmounted } from 'vue'
import { emitter } from './eventBus.js'

const handler = (val) => {
  console.log(val)
}
emitter.on('sendMsg', handler)

onUnmounted(() => {
  emitter.off('sendMsg', handler)
})
</script>

6. 全局状态管理:Pinia

Pinia 是 Vue 官方新一代状态管理库,正式替代 Vuex,完美适配 Vue3 和 TypeScript。

相比 Vuex 的核心优势:

  • 去掉了 mutations,直接修改 state,更简洁
  • 原生支持组合式 API 写法
  • 完美的 TypeScript 类型推导
  • 更轻量,支持模块自动拆分
  • 支持 devtools 调试、热更新

代码示例

javascript 复制代码
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // state
  const username = ref('张三')
  const age = ref(25)
  
  // getters
  const fullInfo = computed(() => `${username.value} - ${age.value}岁`)
  
  // actions
  const updateAge = (newAge) => {
    age.value = newAge
  }
  
  return { username, age, fullInfo, updateAge }
})
vue 复制代码
<!-- 组件中使用 -->
<script setup>
import { useUserStore } from '@/stores/user.js'

const userStore = useUserStore()

// 直接读取
console.log(userStore.username)
console.log(userStore.fullInfo)

// 直接修改或调用方法
userStore.updateAge(26)
userStore.username = '李四'
</script>

7. 补充:defineModel(Vue 3.4+)

Vue 3.4 推出的编译器宏,进一步简化了双向绑定的写法,让组件内部实现 v-model 像声明变量一样简单。

vue 复制代码
<!-- 子组件 -->
<script setup>
// 等价于同时声明了 modelValue 属性和 update:modelValue 事件
const modelValue = defineModel()
</script>

<template>
  <input v-model="modelValue" />
</template>

三、Vue2 vs Vue3 通信方式深度对比

通信场景 Vue2 方案 Vue3 方案 核心差异与变化
基础父子通信 props / $emit、.sync 修饰符 defineProps / defineEmits、多 v-model 语法更简洁;多 v-model 替代 .sync;语义更清晰
隔代透传 attrs / listeners $attrs 移除 listeners,事件统一合并到 attrs,使用更简单
跨层级注入 provide / inject(默认非响应式) provide / inject(原生支持响应式) Vue3 支持 ref/reactive 响应式传递;支持 readonly 保护;能力大幅增强
实例访问 refs、refs、refs、parent、$children ref + defineExpose、$parent(不推荐) 移除 $children;默认封闭组件,必须显式 expose,更强调封装性
事件总线 内置 EventBus(基于 Vue 实例) 移除内置方案,使用 mitt 等第三方库 Vue3 剥离了事件能力,需引入轻量库,功能一致
全局状态管理 Vuex(选项式、mutations/actions 分离) Pinia(组合式、直接修改 state) Pinia 更简洁、TS 友好、无冗余概念,是 Vue3 官方标准方案
设计理念 灵活宽松,提供多种直接访问方式 规范收敛,强调封装与单向数据流 Vue3 牺牲了部分便捷性,换来了更好的可维护性和工程化能力

四、不同场景下的最佳实践建议

  1. 父子组件通信 :优先使用 props + emit / v-model,严格遵循单向数据流,这是最规范、最易维护的方式。
  2. UI 组件二次封装 :使用 $attrs 做属性和事件透传,减少重复代码。
  3. 深层级跨代通信 :优先选择 provide / inject,避免 props 层层传递的"props 地狱"。
  4. 兄弟/无关联组件 :小型项目用 mitt 事件总线;中大型项目统一用 Pinia 管理共享状态。
  5. 操作子组件能力 :使用 ref + defineExpose,只暴露必要的方法和属性,不要滥用。
  6. 全局共享状态:Vue3 项目直接使用 Pinia,不再考虑 Vuex。

结语

从 Vue2 到 Vue3,组件通信的演进本质上是「灵活自由」向「规范工程」的转变。Vue2 提供了大量便捷但容易失控的 API,适合快速开发;Vue3 则收敛了边界,强化了封装和单向数据流,更适合大型团队协作和长期维护的项目。

理解两种版本的设计思路差异,根据项目规模、团队情况选择合适的通信方案,才能写出更优雅、更易维护的 Vue 代码。

相关推荐
DJ斯特拉12 小时前
axios快速使用
开发语言·前端·javascript
还有多久拿退休金12 小时前
Ant Design Tree 搜索定位避坑指南:虚拟滚动下如何实现高亮与精准定位
前端·react.js
Hilaku12 小时前
AI 写代码越快,为什么 Code Review 越不能省?
前端·javascript·程序员
sugar__salt13 小时前
从网页小游戏到数据可视化:掌握 HTML5 Canvas 核心能力
前端·信息可视化·html5
北极星日淘13 小时前
前端 i18n 中日双语交互 + 翻译客服接口联动方案|日系海淘平台中文友好化开发实战
前端·交互
現実逃避と13 小时前
WIN10 Edge连续关闭多个标签页导致资源管理器崩溃临时解决办法
前端·edge
HjhIron13 小时前
CSS 3D 世界:从盒子模型到三维空间动画
javascript·css
VidDown14 小时前
显卡处理视频技术详解:从硬解码到 NVENC,GPU 如何让视频处理起飞?
javascript·编辑器·音视频·视频编解码·视频
jay神14 小时前
基于 FastAPI + Vue 的宠物领养管理系统
前端·vue.js·python·毕业设计·fastapi·宠物
一杯奶茶¥14 小时前
水果销售网站 CRM客户信息管理系统 超市管理系 酒店管理系统 健身房管理系统 在线音乐网站 校园招聘系统
java·vue.js·spring boot·mysql·spring·java项目