Skip to content

Vue 2 组件通信方式

大白话解释: 组件通信就像"家庭成员之间怎么传递信息":

  • 父子通信(Props/Events):父母给孩子东西(Props),孩子告诉父母事情(Events)
  • 兄弟通信(EventBus):兄弟之间通过共同的朋友传话(事件总线)
  • 跨层级通信(Provide/Inject):爷爷直接给孙子东西,不用经过爸爸
  • 全局状态(Vuex):家里有一个公共记账本,谁都能看和改

什么时候用哪种方式?

  • 父子组件:用 Props/Events(最简单、最推荐)
  • 兄弟组件:用 EventBus 或 Vuex
  • 跨层级组件:用 Provide/Inject
  • 复杂状态管理:用 Vuex

1. Props / Events(父子组件)

最基础、最推荐的通信方式。

Props —— 父传子

vue
<!-- 父组件 -->
<template>
  <UserCard
    :name="userName"
    :age="userAge"
    :avatar="userAvatar"
    :tags="userTags"
  />
</template>

<script>
export default {
  data() {
    return {
      userName: '张三',
      userAge: 25,
      userAvatar: '/avatars/1.jpg',
      userTags: ['前端', 'Vue', 'React'],
    }
  },
}
</script>
vue
<!-- 子组件 -->
<script>
export default {
  props: {
    name: { type: String, required: true },
    age: { type: Number, default: 0 },
    avatar: { type: String, default: '/avatars/default.jpg' },
    tags: {
      type: Array,
      default() { return [] },
    },
  },
}
</script>

Events —— 子传父

vue
<!-- 子组件 -->
<script>
export default {
  methods: {
    handleDelete() {
      // 触发 delete 事件,把 id 传给父组件
      this.$emit('delete', this.item.id)
    },
    handleEdit() {
      // 触发 edit 事件,把对象传给父组件
      this.$emit('edit', { id: this.item.id, name: this.item.name })
    },
  },
}
</script>
vue
<!-- 父组件 -->
<template>
  <UserCard
    :user="currentUser"
    @delete="onDelete"
    @edit="onEdit"
  />
</template>

<script>
export default {
  methods: {
    onDelete(id) {
      this.userList = this.userList.filter((u) => u.id !== id)
    },
    onEdit(payload) {
      this.editingUser = payload
    },
  },
}
</script>

v-model —— 双向绑定

⚠️ Vue 3 差异:Vue 2 的 v-model 默认使用 value prop 和 input 事件;Vue 3 改为 modelValue prop 和 update:modelValue 事件。Vue 2 的 model 选项在 Vue 3 中被移除,改用参数化 v-model:title="val" 语法。

vue
<!-- 父组件 -->
<CustomInput v-model="searchText" />
<!-- Vue 2 等价于 -->
<CustomInput :value="searchText" @input="searchText = $event" />
<!-- Vue 3 等价于 -->
<!-- <CustomInput :modelValue="searchText" @update:modelValue="searchText = $event" /> -->
vue
<!-- 子组件 -->
<template>
  <input :value="value" @input="$emit('input', $event.target.value)" />
</template>

<script>
export default {
  model: {
    prop: 'value',   // 默认是 value
    event: 'input',   // 默认是 input
  },
  props: ['value'],
}
</script>

💡 model 选项可以自定义 prop 名和事件名,例如 checkbox 用 prop: 'checked', event: 'change'。Vue 3 中对应写法为 v-model:checked="val"


2. .sync 修饰符

⚠️ Vue 3 已移除 .sync 修饰符。Vue 3 中统一使用带参数的 v-model 替代,如 v-model:visible="showDialog",等价于 Vue 2 的 :visible.sync="showDialog"

vue
<!-- 父组件(Vue 2) -->
<Dialog :visible.sync="showDialog" />
<!-- 等价于 -->
<Dialog :visible="showDialog" @update:visible="showDialog = $event" />

<!-- Vue 3 等价写法 -->
<!-- <Dialog v-model:visible="showDialog" /> -->
vue
<!-- 子组件 -->
<script>
export default {
  props: ['visible'],
  methods: {
    close() {
      this.$emit('update:visible', false)
    },
  },
}
</script>

💡 .sync 可以同时修饰多个属性::title.sync="t" :visible.sync="v"。Vue 3 中对应为 v-model:title="t" v-model:visible="v"


3. $refs —— 父直接访问子

vue
<!-- 父组件 -->
<template>
  <ChildForm ref="childForm" />
  <button @click="submitAll">提交</button>
</template>

<script>
export default {
  methods: {
    submitAll() {
      // 调用子组件方法
      const isValid = this.$refs.childForm.validate()
      if (isValid) {
        const formData = this.$refs.childForm.getFormData()
        this.submit(formData)
      }
    },
  },
}
</script>
vue
<!-- 子组件 -->
<script>
export default {
  data() {
    return { form: { name: '', email: '' } }
  },
  methods: {
    validate() {
      // 表单校验逻辑
      return this.form.name && this.form.email
    },
    getFormData() {
      return { ...this.form }
    },
  },
}
</script>

⚠️ $refs 不是响应式的,不要在模板或 computed 中依赖它。只在事件处理或 mounted 中使用。


4. $parent / $children

js
// 子组件中访问父组件
this.$parent.someMethod()
this.$parent.someData

// 父组件中访问子组件(不推荐,用 $refs 代替)
this.$children[0].someMethod() // 顺序不确定

⚠️ Vue 3 已移除 $children。Vue 3 中不再暴露子组件实例数组,应统一使用 $refs 访问子组件。$parent 仍可用但同样不推荐。

⚠️ 强烈不推荐。紧耦合,难以维护和测试。优先用 props/events/$refs。


5. provide / inject(跨层级)

祖先组件向任意深度的后代组件传递数据,无需逐层传递 props。

js
// 祖先组件
export default {
  provide() {
    return {
      // ⚠️ 非响应式 —— 传的是值,不是引用
      theme: this.theme,

      // ✅ 响应式方案一:传递整个响应式对象
      appConfig: this.config, // config 是一个响应式对象

      // ✅ 响应式方案二:函数式
      getTheme: () => this.theme,
    }
  },
  data() {
    return {
      theme: 'dark',
      config: { primaryColor: '#1890ff', fontSize: 14 },
    }
  },
}
js
// 后代组件(任意层级)
export default {
  inject: ['theme', 'appConfig', 'getTheme'],

  computed: {
    // 通过函数式获取响应式数据
    currentTheme() {
      return this.getTheme()
    },
    // 直接访问响应式对象
    primaryColor() {
      return this.appConfig.primaryColor
    },
  },
}

provide/inject 的坑

js
// ❌ 非响应式 —— 祖先的 theme 变了,后代的 this.theme 不会变
provide() {
  return { theme: this.theme } // 传的是基本类型的值
}

// ✅ 响应式 —— 传对象引用
provide() {
  return { themeConfig: this.themeConfig } // 传的是对象引用
}

6. EventBus(事件总线)

⚠️ Vue 3 中 EventBus 不再可用。Vue 3 移除了 $on / $off / $once 实例方法,无法通过 new Vue() 创建事件总线。推荐使用第三方库 mitttiny-emitter 替代。

任意组件间通过事件通信,适合简单场景。

创建

js
// main.js
Vue.prototype.$bus = new Vue()

// 或单独文件
// utils/eventBus.js
import Vue from 'vue'
export default new Vue()

使用

vue
<!-- 组件 A —— 发送方 -->
<script>
export default {
  methods: {
    login() {
      // 登录成功后
      this.$bus.$emit('user:login', {
        id: 1,
        name: '张三',
        token: 'xxx',
      })
    },
  },
}
</script>
vue
<!-- 组件 B —— 接收方 -->
<script>
export default {
  created() {
    this.$bus.$on('user:login', this.handleLogin)
  },
  beforeDestroy() {
    // ✅ 必须取消监听,否则内存泄漏
    this.$bus.$off('user:login', this.handleLogin)
  },
  methods: {
    handleLogin(user) {
      console.log(`${user.name} 登录了`)
      this.fetchNotifications()
    },
  },
}
</script>

监听一次

js
// 只触发一次
this.$bus.$once('app:ready', () => {
  console.log('应用准备就绪')
})

取消所有监听

js
// 取消某个事件的所有监听
this.$bus.$off('user:login')

// 取消所有事件的所有监听(慎用)
this.$bus.$off()

⚠️ EventBus 的问题:事件名容易拼错、难以追踪数据流、大项目会变成"事件地狱"。中大型项目建议用 Vuex。


7. Vuex(全局状态管理)

适合多组件共享的复杂状态,详见 Vuex 状态管理


8. $attrs / $listeners(跨层级透传)

⚠️ Vue 3 已移除 $listeners。Vue 3 中事件监听器已合并到 $attrs 中,不再需要单独访问 $listeners$attrs 现在包含所有未被 props 声明的属性(包括 class、style 和事件监听器)。

将父组件的非 props 属性和事件监听器透传给深层子组件。

vue
<!-- 祖先组件 -->
<BaseInput type="text" placeholder="请输入" @focus="onFocus" />
vue
<!-- 中间组件 BaseInput.vue -->
<template>
  <!-- v-bind="$attrs" 透传非 props 属性 -->
  <!-- v-on="$listeners" 透传事件监听 -->
  <input v-bind="$attrs" v-on="$listeners" class="base-input" />
</template>

<script>
export default {
  inheritAttrs: false, // 不自动应用到根元素
}
</script>
vue
<!-- 深层子组件中也可以继续透传 -->
<template>
  <div class="wrapper">
    <InnerInput v-bind="$attrs" v-on="$listeners" />
  </div>
</template>

9. 兄弟组件通信方案对比

方案一:通过共同父组件中转

vue
<!-- 父组件 -->
<template>
  <ChildA @message="onMessage" />
  <ChildB :message="messageFromA" />
</template>

<script>
export default {
  data() { return { messageFromA: '' } },
  methods: {
    onMessage(msg) { this.messageFromA = msg },
  },
}
</script>

方案二:EventBus

js
// 兄弟 A
this.$bus.$emit('brother:msg', data)

// 兄弟 B
this.$bus.$on('brother:msg', (data) => { ... })

方案三:Vuex

js
// 兄弟 A
this.$store.commit('SET_MESSAGE', data)

// 兄弟 B
computed: {
  message() { return this.$store.state.message }
}

方案选择指南

场景推荐方案理由
父 → 子props最简单,类型安全
子 → 父$emit / .sync单向数据流
父访问子$refs直接调用子组件方法
祖先 → 后代provide/inject避免逐层传递 props
兄弟组件父组件中转 / EventBus / Vuex按复杂度选择
任意组件Vuex大型项目,状态可追踪
跨层级透传$attrs / $listeners包装组件场景

参考

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准