vue 中常见的 8 种常规通信方案

  • 通过 props 传递
  • 通过 $emit 触发自定义事件
  • 使用 ref
  • EventBus
  • $parent 或 $root
  • attrs 和 listeners
  • Provide 与 Inject
  • Vuex 和 Pinia

组件间通信的分类可以分成以下:

  • 父子关系组件选择 props 与 $emit ,或者 ref
  • 兄弟关系组件选择 $bus 和 $parent
  • 祖先与后代关系组件选择 attrs 与 listeners 或 Provide 与 Inject
  • 复杂关系组件选择 vuex 或 Pinia

1. 父子组件通信 (Props / Emit / ref)

1.1. Props(父 -> 子)

Props 是 Vue 中实现父组件向子组件传递数据的核心方式,它遵循了单向数据流的原则。

具体实现分为两步:

  • 父组件 (传递方):在模板的子组件标签上,通过属性绑定(使用 : 或 v--bind)的方式,将父组件自身的数据传递下去。
  • 子组件 (接收方):在 <script setup> 中,使用 defineProps 宏来声明并接收从父组件传递来的 props。

子组件:

<script setup lang='js'>
  import { ref } from 'vue'
  import Child from '../../components/Child.vue'

  const data = ref('父组件数据')
</script>

<template>
  <Child :data="data" />
</template>

父组件:

<script setup lang='js'>
  const props = defineProps({
    data: {
      type: String,
      default: ''
    }
  })
</script>

<template>
  <div class=''>
    Child {{ props.data }}
  </div>
</template>

1.2. emit(子 -> 父)

子组件向父组件通信,主要使用 emit 机制。首先,在子组件的 <script setup> 中,必须使用 defineEmits 来声明它将要触发的自定义事件名。然后,在需要的时候,调用 defineEmits 返回的 emit 函数,触发该事件并可以附带任意参数。在父组件中,通过在子组件标签上使用 @ 符号(即 v-on)来监听这个自定义事件,并绑定一个处理函数。当事件被触发时,这个处理函数就会被调用,并且其参数列表会按顺序接收到子组件 emit 出来的所有值,从而实现了自下而上的数据传递。

子组件:

<script setup lang='js'>
  const emit = defineEmits(['custom-event'])

  const handleClick = () => {
    emit('custom-event', '子组件传值')
  }
</script>

<template>
  <button @click="handleClick">点击传值</button>
</template>

父组件:

<script setup lang='js'>
  import Child from '../../components/Child.vue'

  const handleCustomEvent = (value) => {
    console.log(value)
  }
</script>

<template>
  <Child :data="data" @custom-event="handleCustomEvent" />
</template>

1.3. Ref

1.3.1. 父 -> 子

通过 ref 实现父组件向子组件的命令式通信:首先,在父组件的模板中,为子组件标签添加 ref 属性以获取其实例;然后,在父组件的逻辑中,通过访问这个 ref 实例直接调用子组件暴露的方法,并将需要传递的数据作为方法参数传入。子组件对应的方法在接收到参数后,即可执行相应操作。

父组件:

<script setup lang='js'>
  import { ref } from 'vue'
  import Child from '../../components/Child.vue'

  const data = ref('父组件传递的数据')

  const ChildRef = ref(null)

  const handleChild = () => {
    ChildRef.value.init(data)
  }
</script>

<template>
  <div class=''>
    <Child ref="ChildRef" />
    <button @click="handleChild">传递数据</button>
  </div>
</template>

子组件:

<script setup lang='js'>
  import { ref } from 'vue'
  import GrandChild from './GrandChild.vue'

  let data = ref('')

  const init = (value) => {
    data.value = value
    console.log(value)
  }

  defineExpose({ init })
</script>

<template>
  <div class=''>
    Child
    <GrandChild />
  </div>
</template>
1.3.2. 子 -> 父

在 Vue 3 中,父组件可以通过 ref 来获取子组件的实例,并主动调用子组件暴露的方法或访问其数据。

这个过程分为两步:

  • 子组件(被动方):在 <script setup> 中,子组件必须使用 defineExpose 宏,显式地将其希望被父组件访问的属性和方法暴露出去,形成一个公开的 API。
  • 父组件(主动方):在父组件模板中,通过在子组件标签上设置 ref="childRef",并将 childRef 关联到一个 ref() 对象,即可获取到子组件的实例。之后,父组件就可以在适当的时机(如 onMounted 或事件回调中),通过 childRef.value 来访问子组件暴露的 API

子组件:

<script setup lang='js'>
  import { ref } from 'vue'
  import GrandChild from './GrandChild.vue'

  let ChildData = ref('子组件的数据')

  defineExpose({ ChildData })
</script>

<template>
  <div class=''>
    Child
    <GrandChild />
  </div>
</template>

父组件:

<script setup lang='js'>
  import { onMounted } from 'vue'
  import Child from '../../components/Child.vue'

  onMounted(() => {
    if (ChildRef.value) {
      console.log(ChildRef.value.ChildData);
    }
  })
</script>

<template>
  <div class=''>
    <Child ref="ChildRef" />
  </div>
</template>

2. 祖孙/跨级组件通信 (Provide / Inject)

provide 和 inject 是 Vue 官方推荐的跨级组件通信方案。它允许一个祖先组件 (Provider) 作为依赖的提供者,将其数据或方法提供 (provide) 给其后代组件树中的任何一个组件。

最核心的优势在于,这个过程具有“穿透性”。无论组件层级有多深,任何中间层组件都无需进行任何额外的 props 声明或传递。最终的后代组件 (Injector) 只需要通过 inject 并使用与提供者相同的 key,就能精准地接收到这份依赖。

简单来说:祖先组件使用 provide 给孙子传递组件,中间层什么都不需要做,在孙子组件使用 inject 接收

祖先组件:

<script setup lang='js'>
  import { ref, provide, readonly } from 'vue'
  import Child from '../../components/Child.vue'

  const data = ref('祖先数据')

  provide('data', data) // readonly(data) 使用 readonly 包裹代表只读,孙子不可修改

</script>

<template>
  <div class=''>
    <Child />
  </div>
</template>

孩子(中间层):

<script setup lang='js'>
  import GrandChild from './GrandChild.vue'
</script>

<template>
  <div class=''>
    Child  <GrandChild />
  </div>
</template>

孙子组件:

<script setup lang='js'>
  import { inject } from 'vue';

  let data = inject('data');

  console.log("祖先传递过来的数据:", data.value);

</script>

<template>
  <div class=''>
    {{ data }}
  </div>
</template>

3. 祖孙组件通信($attrs 与 listeners)

孙子向祖先通信,是通过事件透传实现的:祖先在父组件上监听事件,父组件通过 v-bind="$attrs" 将该监听器透传给孙子,最终由孙子组件 emit 触发。

祖先向孙子传值,是一个自上而下的数据流:祖先传递的属性,父组件通过 v-bind="$attrs",最终被孙子组件的 props 接收,完美解决了“属性逐层传递”的问题。

祖先组件:

<script setup lang='js'>
  import { ref, onMounted } from 'vue'
  import Child from '../../components/Child.vue'

  // 准备要传递的数据
  const messageFromGrand = ref('来自祖先的问候!');
  const userFromGrand = ref({ name: '张三' });

  // 准备一个方法来处理来自孙子组件的响应
  const handleChildResponse = (dataFromChild) => {
    console.log('祖先组件收到了来自孙子的响应:', dataFromChild);
    console.log(`祖先收到了来自 ${dataFromChild.from} 的消息: ${dataFromChild.text}`);
  };

</script>

<template>
  <div class="grandparent-component">
    <h1>我是祖先组件 (Grandparent)</h1>
    <Child parentTitle="父组件的专属标题" :message="messageFromGrand" :user="userFromGrand" @response="handleChildResponse" />
  </div>
</template>

<style scoped>
  .grandparent-component {
    border: 2px solid #3498db;
    padding: 20px;
  }
</style>

父组件:

<script setup lang='js'>
import { ref } from 'vue'
import GrandChild from './GrandChild.vue'

// 1. 【关键】Parent 组件只声明它自己真正关心的 prop
const props = defineProps({
  parentTitle: {
    type: String,
    required: true
  }
});

</script>

<template>
  <div class='parent-component'>
    <h2>我是父组件 (Parent)</h2>
    <p>我只关心我自己的标题: <strong>{{ props.parentTitle }}</strong></p>
    <p>其他来自祖先的东西,我直接透传给 Child...</p>

    <!-- 
      2. 【核心】使用 v-bind="$attrs"
      $attrs 对象包含了所有从 Grandparent 传递过来,但没有被 Parent 的 props
      (即 parentTitle) 接收的“剩余”属性和事件。
      v-bind="$attrs" 会将这些属性和事件监听器,原封不动地应用到 Child 组件上。
    -->
    <GrandChild v-bind="$attrs" />
  </div>
</template>


<style scoped>
.parent-component {
  border: 2px solid #e67e22;
  padding: 20px;
  margin-top: 20px;
}
</style>

孙子组件:

<script setup>
// 1. 声明接收来自祖先的 props
const props = defineProps({
  message: {
    type: String,
    default: '默认消息'
  },
  user: {
    type: Object,
    default: () => ({ name: '默认用户' })
  }
});

// 2. 声明会触发的事件
const emit = defineEmits(['response']);

const handleChildClick = () => {
  // 3. 触发事件,并回传数据
  const responseData = { from: 'Child', text: '你好,祖先!' };
  emit('response', responseData);
};
</script>

<template>
  <div class="child-component">
    <h3>我是孙子组件 (Child)</h3>
    <p>接收到的消息: <strong>{{ props.message }}</strong></p>
    <p>接收到的用户: <strong>{{ props.user.name }}</strong></p>
    <button @click="handleChildClick">点击我,向祖先响应</button>
  </div>
</template>

<style scoped>
.child-component {
  border: 2px solid #42b983;
  padding: 20px;
  margin-top: 20px;
}
</style>

4. EventsBus

main.js

import Vue from 'vue'
Vue.prototype.$bus = new Vue()

发送:

this.$bus.$emit('custom-event', 'hello')

接收:

this.$bus.$on('custom-event', msg => {
  console.log('接收到:', msg)
})

5. root

传递方:

<template>
  <Child />
</template>

<script setup>
  import { getCurrentInstance } from 'vue'
  import Child from '../../components/Child.vue'

  const app = getCurrentInstance()
  app.appContext.config.globalProperties.$rootMsg = "来自 $root 的全局信息"
</script>

接收方:

<template>
  <div>
    <p>孙组件获取的 root 信息:{{ rootMsg }}</p>
  </div>
</template>

<script setup>
import { getCurrentInstance } from 'vue'

const app = getCurrentInstance()
const rootMsg = app.appContext.config.globalProperties.$rootMsg
console.log(rootMsg);
</script>

更多推荐