vue3的组件通信方式汇总

在 Vue3 中,组件通信方式根据组件关系(父子、祖孙、兄弟、跨级等)的不同,有多种实现方式,所以选择合适的通信方式可以使我们的开发事半功倍。

一、Props / Emits

1.1 Props (父向子通信)

适用场景: 父子组件之前传值、兄弟组件通过父组件传值通信

原理:父组件通过属性绑定传递数据,子组件通过 props 接收并使用。

实现步骤:

  1. 父组件在引用子组件时,通过 v-bind(简写 :)传递数据:

    <template>
      <div class="comp-parent">
        <!-- title属于静态属性 通过v-bind绑定响应式数据count属性 -->
        <CompChild title="子组件标题" :count="count" />
      </div>
    </template>
    <script setup lang="ts">
    import { ref } from 'vue'
    import CompChild from './comp-child.vue'
    const count = ref(0)
    </script>
    
    
  2. 子组件通过 defineProps 定义接收的 props(支持类型校验、默认值等):

<template>
  <div class="comp-child">
    <h1>{{ title }}</h1>
    <p>当前计数:{{ count }}</p>
  </div>
</template>

<script setup lang="ts">
// 方式一:通过defineProps定义子组件接收的属性
// defineProps({
//   title: {
//     type: String,
//     default: '',
//   },
//   count: {
//     type: Number,
//     default: 0,
//   },
// })
// 方式二: 使用 TypeScript 类型注解
defineProps<{
  title: string
  count?: number
}>()
</script>

1.2 Emits(自定事件:子向父传值)

适用场景: 子组件向上传值

原理: 子组件通过触发自定义事件传递数据,父组件监听事件并接收数据。

实现步骤:

  1. 子组件通过触发自定义事件传递数据,父组件监听事件并接收数据。

    <template>
      <div class="comp-parent">
        <!-- title属于静态属性 通过v-bind绑定响应式数据count属性, 子组件通过addCount事件触发父组件更新count -->
        <CompChild title="子组件标题" :count="count" @addCount="addCount" />
      </div>
    </template>
    
    <script setup lang="ts">
    import { ref } from 'vue'
    import CompChild from './comp-child.vue'
    const count = ref(0)
    // 定义addCount方法
    const addCount = (val: number) => {
      console.log('接受子组件count加一事件: ', val)
      count.value = val
    }
    </script>
    
    
  2. 父组件监听子组件的自定义事件,通过回调接收数据:

<template>
  <div class="comp-child">
    <h1>{{ title }}</h1>
    <p>当前计数:{{ count }}</p>
    <button @click="handleClick">count加一</button>
  </div>
</template>

<script setup lang="ts">
const props = defineProps<{
  title: string
  count?: number
}>()
// 使用 defineEmits
const emit = defineEmits(['addCount'])

const handleClick = () => {
  if (typeof props?.count !== 'undefined') {
    emit('addCount', props.count + 1)
  }
}
</script>

二、 V-model(父子双向绑定)

适用场景: 父子组件传值通信

原理: 基于 propsemits 的语法糖,简化父子组件数据双向同步。

实现步骤:

  1. 父组件使用 v-model 绑定数据:

    <template>
      <div class="comp-parent">
        <CompChild v-model:count="count" />
        <!-- 等价于:<CompChild :modelValue="count" @update:modelValue="count = $event" /> -->
      </div>
    </template>
    
    <script setup lang="ts">
    import { ref } from 'vue'
    import CompChild from './comp-child.vue'
    const count = ref(0)
    </script>
    
    
  2. 子组件通过 modelValue 接收数据,通过 update:modelValue 事件更新:

    <template>
      <div class="comp-child">
        <p>当前计数:{{ count }}</p>
        <button @click="handleClick">count加一</button>
      </div>
    </template>
    
    <script setup lang="ts">
    const props = defineProps<{
      count: number
    }>()
    // 使用 defineEmits
    const emit = defineEmits(['update:count'])
    const handleClick = () => {
      if (typeof props?.count !== 'undefined') {
        emit('update:count', props.count + 1)
      }
    }
    </script>
    
    

三、 ref / 模板引用(父组件访问子组件实例)

适用场景: 父子组件传值

原理: 父组件通过 ref 获取子组件实例,直接访问子组件的属性或方法(需子组件主动暴露)。

实现步骤:

  1. 子组件通过 defineExpose 暴露需要被父组件访问的内容:

    <template>
      <div class="comp-child">
        <h1>{{ title }}</h1>
      </div>
    </template>
    
    <script setup lang="ts">
    import { ref } from 'vue'
    const title = ref<string>('子组件标题')
    // 暴露给父组件属性和方法
    defineExpose({
      title,
      changeTitle(newTitle: string) {
        title.value = newTitle
      },
    })
    </script>
    
    
  2. 父组件通过 ref 绑定子组件,获取实例并访问:

<template>
  <div class="comp-parent">
    <CompChild ref="childRef" />
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import CompChild from './comp-child.vue'
// 子组件实例引用
const childRef = ref<typeof CompChild>()
// 实例挂载完成后调用
onMounted(() => {
  console.log('子组件标题:', childRef.value?.title)
  childRef.value?.changeTitle('父组件更改的子组件标题')
})
</script>

四、Provide / Inject (依赖注入)

适用场景: 父子组件传值、祖先组件传值

原理: 祖先组件通过 provide 提供数据,任意后代组件通过 inject 注入并使用,无视层级。

使用步骤:

  1. 祖先组件通过provide提供数据:

    <template>
      <div class="comp-parent">
        <CompChild />
      </div>
    </template>
    
    <script setup lang="ts">
    import { ref, provide } from 'vue'
    import CompChild from './comp-child.vue'
    // 用户信息
    const userInfo = ref({
      name: '懒羊羊我小弟',
      age: 18,
    })
    // 传给子孙的属性和方法
    provide('userInfo', userInfo)
    </script>
    
    
  2. 后代组件(子、孙等)通过inject 注入数据:

    <template>
      <div class="comp-child">
        <h1>{{ userInfo?.name }}</h1>
      </div>
    </template>
    <script setup lang="ts">
    // 接收祖先传递的参数
    import { inject } from 'vue'
    // 接收祖先传递的用户信息
    const userInfo = inject<{ name: string; age: number }>('userInfo')
    </script>
    

五、 Event Bus (全局事件总线)

适用场景: 兄弟组件,没有直接关系的组件

原理:通过一个全局的事件中心,组件可触发事件或监听事件,实现解耦通信。

实现步骤:

  1. 定义一个事件总线实例

    // eventBus.js
    import { ref } from 'vue'
    
    class EventBus {
      constructor() {
        this.events = ref({})
      }
    
      $on(event, callback) {
        if (!this.events.value[event]) {
          this.events.value[event] = []
        }
        this.events.value[event].push(callback)
      }
    
      $off(event, callback) {
        if (!this.events.value[event]) return
        if (callback) {
          this.events.value[event] = this.events.value[event].filter((cb) => cb !== callback)
        } else {
          delete this.events.value[event]
        }
      }
    
      $emit(event, ...args) {
        if (this.events.value[event]) {
          this.events.value[event].forEach((callback) => {
            callback(...args)
          })
        }
      }
    }
    
    export const eventBus = new EventBus()
    
    // 或者使用 mitt 库
    // import mitt from 'mitt'
    // export const eventBus = mitt()
    
    
  2. 在需要使用的组件中引入实例并监听自定函数

    <!--> 父组件<-->
    <template>
      <div class="comp-parent">
        <CompChild />
      </div>
    </template>
    
    <script setup lang="ts">
    import { onMounted } from 'vue'
    import CompChild from './comp-child.vue'
    import { eventBus } from '@/utils/event-bus'
    
    // 触发事件
    onMounted(() => {
      eventBus.$emit('userInfoChange', { name: '懒羊羊我小弟', age: 18 })
    })
    </script>
    
    <!--> 子组件组件<-->
    <template>
      <div class="comp-child">
        <h1>{{ userInfo?.name }}</h1>
      </div>
    </template>
    <script setup lang="ts">
    // 接收祖先传递的参数
    import { ref, onMounted, onUnmounted } from 'vue'
    import { eventBus } from '@/utils/event-bus'
    const userInfo = ref<{ name: string; age: number }>({ name: '', age: 0 })
    // 监听事件
    onMounted(() => {
      eventBus.$on('userInfoChange', (newUserInfo: { name: string; age: number }) => {
        userInfo.value = newUserInfo
      })
    })
    // 取消监听事件
    onUnmounted(() => {
      eventBus.$off('userInfoChange')
    })
    </script>
    
    

六、 Attrs(属性透传)

适用场景: 父子组件通信、组件库组件的属性继承

原理: 当组件嵌套层级较深时(如 A → B → C),若 A 要给 C 传递属性,无需 B 手动通过 props 接收再传递给 C,而是可以通过 B 中的 $attrs 直接 “透传” 给 C,减少中间层的代码冗余。

实现步骤:

  1. 传递属性给子组件,但子组件不声明 props

    <template>
      <div class="comp-parent">
        <CompChild id="comp-child" :userInfo="userInfo" @changeTitle="changeTitle" />
      </div>
    </template>
    
    <script setup lang="ts">
    import { ref } from 'vue'
    import CompChild from './comp-child.vue'
    const userInfo = ref<{ name: string; age: number }>({ name: '懒羊羊我小弟', age: 18 })
    // 定义一个方法,用于改变子组件的title
    const changeTitle = (title: string) => {
      userInfo.value.name = title
    }
    </script>
    
    
  2. 子组件通过useAttrs获取到未被defineProps获取的属性值

    <template>
      <div class="comp-child">
        <h1>{{ userInfo?.name }}</h1>
      </div>
    </template>
    <script setup lang="ts">
    import { useAttrs, onMounted } from 'vue'
    // 获取未被defineProps定义的属性
    const $attrs = useAttrs()
    console.log('attrs: ', $attrs)
    
    // 接收需要的props传参
    defineProps({
      userInfo: {
        type: Object,
        default: () => {},
      },
    })
    
    onMounted(() => {
      if ($attrs.onChangeTitle) {
        // 调用子组件传递的方法
        ;($attrs.onChangeTitle as (title: string) => void)('懒羊羊我大哥')
      }
    })
    </script>
    
    

    补充:

    1. 默认情况下,$attrs 中的属性会自动添加到组件的根元素上(除了已被 props 声明的)。若想禁用这一行为,可在组件中设置 inheritAttrs: false

      <!-- 子组件 Child.vue -->
      <template>
        <div>子组件</div>
        <Grandchild v-bind="$attrs" /> <!-- 手动透传给孙组件 -->
      </template>
      
      <script setup>
        // Vue3 中在 setup 外声明 inheritAttrs
        export default {
          inheritAttrs: false // 根元素不再自动添加 $attrs 中的属性
        }
      </script>
      
    2. $attrs 中的属性是 “剩余” 未被声明的属性,遵循 “声明优先” 原则。

    3. 可以选择性透传:v-bind="{ ...$attrs, otherProp: 'value' }"(扩展运算符)。

    4. 避免过度透传$attrs 适合简单的透传场景,若透传层级过深或属性 / 事件过多,建议使用 provide/inject 或状态管理库,避免维护困难。

    七、Slots(插槽通信)

适用场景: 子组件向父组件通信

原理: 子组件在定义插槽时,通过 v-bind(或简写 :)将内部数据绑定到插槽上,这些数据会成为 “插槽作用域” 的一部分,供父组件使用。

实现步骤:

  1. 在子组件中预留插槽位置,将需要传递出去的数据通过v-bind绑定

    <template>
      <div class="comp-child">
        <!-- 定义header插槽,接收title属性 -->
        <slot name="header" :title="title"></slot>
      </div>
    </template>
    <script setup lang="ts">
    import { ref } from 'vue'
    const title = ref<string>('懒羊羊我小弟')
    </script>
    
    
  2. 父组件通过插槽来获取子组件数据展示到页面上

    <template>
      <div class="comp-parent">
        <CompChild>
          <template #header="{ title }">
            <h1>{{ title }}</h1>
          </template>
        </CompChild>
      </div>
    </template>
    
    <script setup lang="ts">
    import { useSlots } from 'vue'
    import CompChild from './comp-child.vue'
    const slots = useSlots()
    console.log('slots: ', slots.header)
    </script>
    
    

八、Pina、VueX(状态管理库)

适用场景: 适用各种场景的组件通信

原理: 通过第三方状态管理库,去开辟不同的独立存储空间,可以处理各种场景的数据通信;

实现步骤(以Pina为例):

  1. 定义pina储存仓库

    import { defineStore } from 'pinia'
    import { ref } from 'vue'
    
    export const useUserStore = defineStore('user', () => {
      // Composition API 风格
      const user = ref<{ name: string; age: number } | null>(null)
      function login(userData: { name: string; age: number }) {
        console.log('userData: ', userData)
    
        user.value = userData
      }
    
      return {
        user,
        login,
      }
    })
    
    
  2. 在仓库中获取要展示的数据

    <!--> 子组件 <-->
    <template>
      <div class="comp-child">
        <h1>{{ user?.name }}</h1>
      </div>
    </template>
    <script setup lang="ts">
    import { storeToRefs } from 'pinia'
    // 引入 用户信息userStore
    import { useUserStore } from '@/stores/user'
    // 从userStore中获取user状态
    const userStore = useUserStore()
    // 通过storeToRefs将user状态转换为响应式引用
    const { user } = storeToRefs(userStore)
    </script>
    
    
  3. 调用方法更改仓库数据

    <!--> 父组件 <-->
    <template>
      <div class="comp-parent">
        <CompChild />
      </div>
    </template>
    
    <script setup lang="ts">
    import { useUserStore } from '@/stores/user'
    import CompChild from './comp-child.vue'
    // 引入 用户信息userStore
    const userStore = useUserStore()
    // 调用userStore的login方法,登录用户
    userStore.login({ name: '懒羊羊我小弟', age: 18 })
    </script>
    
    

九、 总结

通信方式适用场景优点缺点
Props/Emits父子组件通信简单直接,类型安全不适合深层组件
v-model表单组件双向绑定语法简洁,符合习惯仅限于表单场景
ref调用子组件方法直接访问子组件破坏组件封装性
Provide/Inject深层组件通信避免逐层传递数据流向不明确
Event Bus任意组件通信完全解耦,灵活难以调试和维护
Pinia复杂状态管理功能强大,可调试需要学习成本
Attrs属性透传灵活处理未知属性可能造成属性冲突
Slots内容分发高度灵活,可定制语法相对复杂

选择建议:

  • 简单父子通信:Props/Emits
  • 表单组件:v-model
  • 深层组件:Provide/Inject
  • 复杂应用状态:Pinia
  • 临时全局事件:Event Bus
  • UI 组件库:Slots + Attrs

根据具体场景选择合适的通信方式,保持代码的可维护性和可读性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

懒羊羊我小弟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值