Vue组件通信全攻略:从父子传到事件总线,玩转组件数据流!

Vue组件通信全攻略:从父子传到事件总线,玩转组件数据流!

引言:为什么组件需要"聊天"?

大家好!在Vue应用开发中,组件就像是一个个独立的"小房间",但有时候这些房间需要互相"传纸条"交流信息。这就是我们今天要聊的组件通信!🏠➡️🏠

想象一下,如果没有通信机制,我们的组件就像是在孤岛上的鲁滨逊,无法协作完成复杂的任务。那么Vue为我们提供了哪些"传纸条"的方式呢?让我们一起来探索吧!

第一章:Vue2时代的"老派"通信方式

选项式API:像搭积木一样直观

在Vue2时期,我们主要使用选项式API,这种写法非常直观,就像是在填写一个配置表格:

javascript 复制代码
// 老派的类式写法,新手友好!
export default {
  // 数据部分
  data() {
    return {
      msg1: '这是给子组件的信息1'
    }
  },
  // 方法部分
  methods: {
    handleClick() {
      console.log('被点击了!')
    }
  },
  // 注册组件
  components: {
    ChildComponent
  }
}

优点 :结构清晰,学习曲线平缓,适合Vue新手 缺点:逻辑关注点分散,大型项目中维护困难

第二章:父子组件通信的"双向奔赴"

2.1 父传子:Props的爱心快递 📦

父组件:我是慷慨的爸爸,总给孩子好东西

vue 复制代码
<template>
  <div>
    <h1>我是父组件</h1>
    <!-- 通过props传递数据,就像给孩子零花钱 -->
    <ChildComponent :money="pocketMoney" :advice="fathersWords"/>
  </div>
</template>

<script setup>
import ChildComponent from './ChildComponent.vue'
import { ref } from 'vue'

const pocketMoney = ref(100) // 零花钱
const fathersWords = ref('要好好学习!') // 爸爸的叮嘱
</script>

子组件:谢谢爸爸的礼物!🎁

vue 复制代码
<template>
  <div>
    <p>我收到了{{ money }}元零花钱!</p>
    <p>爸爸说:{{ advice }}</p>
  </div>
</template>

<script setup>
// 定义接收的props,就像拆礼物前的期待
defineProps({
  money: {
    type: Number,
    default: 0 // 默认值,万一爸爸忘了给呢
  },
  advice: {
    type: String,
    default: '要听话'
  }
})
</script>

2.2 子传父:Emit的情感表达 💌

有时候,孩子也需要向父母表达自己的想法!

Emit相当于自定义事件,且可以获得子组件传来的数据的事件

子组件:爸爸,我有话要说!

vue 复制代码
<template>
  <div>
    <button @click="sayHello">跟爸爸打招呼</button>
    <button @click="askForMoney">请求更多零花钱</button>
  </div>
</template>

<script setup>
// 定义要发射的事件类型
const emit = defineEmits(['greet', 'requestMoney'])

const sayHello = () => {
  emit('greet', '爸爸你好!') // 第一个参数是事件名,第二个是数据
}

const askForMoney = () => {
  emit('requestMoney', '零花钱不够用了😭')
}
</script>

父组件:孩子长大了,会跟我交流了!🥹

vue 复制代码
<template>
  <div>
    <ChildComponent 
      @greet="handleGreet" 
      @requestMoney="handleMoneyRequest"
    />
    <p>孩子对我说:{{ childMessage }}</p>
    <p>钱包状态:{{ wallet }}元</p>
  </div>
</template>

<script setup>
import ChildComponent from './ChildComponent.vue'
import { ref } from 'vue'

const childMessage = ref('')
const wallet = ref(1000)

const handleGreet = (message) => {
  childMessage.value = message
  console.log('收到孩子的问候:', message)
}

const handleMoneyRequest = (reason) => {
  console.log('孩子要钱的理由:', reason)
  if (wallet.value >= 100) {
    wallet.value -= 100
    console.log('给了100元零花钱')
  } else {
    console.log('爸爸也没钱了😅')
  }
}
</script>

2.3 更直接的交流:Expose的"后门"通道 🚪

有时候父组件需要直接调用子组件的方法,这时候就需要defineExpose来开个"后门":

子组件:这些是我的特殊技能!

vue 复制代码
<template>
  <div>我是有特殊技能的组件</div>
</template>

<script setup>
const specialSkill = '我会隐身术!'

const showSkill = () => {
  console.log('展示技能:', specialSkill)
}

const hiddenMethod = () => {
  console.log('这是隐藏方法,外部不能直接调用')
}

// 只暴露想让父组件访问的内容
defineExpose({
  specialSkill,
  showSkill
  // hiddenMethod 没有被暴露,所以父组件访问不到
})
</script>

父组件:让我看看你有什么本事!

vue 复制代码
<template>
  <div>
    <ChildComponent ref="childRef" />
    <button @click="checkChildSkill">检查孩子技能</button>
  </div>
</template>

<script setup>
import ChildComponent from './ChildComponent.vue'
import { ref } from 'vue'

const childRef = ref(null) // 创建子组件的引用

const checkChildSkill = () => {
  if (childRef.value) {
    console.log('孩子的技能:', childRef.value.specialSkill)
    childRef.value.showSkill() // 直接调用子组件方法
    
    // childRef.value.hiddenMethod() // 这会报错,因为方法没有被暴露
  }
}
</script>

第三章:跨层级通信的"心灵感应" 🔮

Provide/Inject:祖孙之间的"隔代遗传"

当组件层级很深时,逐层传递props就太麻烦了!这时候provideinject就像是组件间的"心灵感应":

祖先组件:我为后代准备了一些家族遗产

vue 复制代码
<template>
  <div>
    <h1>我是祖爷爷组件</h1>
    <MiddleComponent />
  </div>
</template>

<script setup>
import MiddleComponent from './MiddleComponent.vue'
import { provide, ref } from 'vue'

const familyFortune = ref(1000000) // 家族财富
const familyMotto = '勤劳致富' // 家训

// 提供数据给所有后代组件
provide('fortune', familyFortune)
provide('motto', familyMotto)
provide('increaseFortune', (amount) => {
  familyFortune.value += amount
})
</script>

中间组件:我什么都不做,只是路过... 🚶

vue 复制代码
<template>
  <div>
    <p>我是中间组件,我不管数据传输</p>
    <GrandChildComponent />
  </div>
</template>

<script setup>
import GrandChildComponent from './GrandChildComponent.vue'
// 注意:中间组件不需要任何props传递!
</script>

后代组件:直接拿到祖辈的遗产!💰

vue 复制代码
<template>
  <div>
    <p>我继承了{{ inheritance }}元家产!</p>
    <p>我们的家训是:{{ motto }}</p>
    <button @click="workHard">努力工作增加家产</button>
  </div>
</template>

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

// 直接注入祖先提供的数据,跳过中间所有组件
const inheritance = inject('fortune')
const motto = inject('motto')
const increaseFortune = inject('increaseFortune')

const workHard = () => {
  increaseFortune(1000)
  console.log('家产增加了!现在有:', inheritance.value)
}
</script>

第四章:任意组件间的"广播系统" 📢

事件总线:组件界的"微信群聊"

当组件之间没有直接的父子关系,但又需要通信时,事件总线就派上用场了!

第一步:创建我们的"微信群"

javascript 复制代码
// mitt.js - 这就是我们的微信群!
import mitt from 'mitt'
const eventBus = mitt() // 创建事件总线实例
export default eventBus

第二步:有人在群里发消息

vue 复制代码
<!-- PublisherComponent.vue -->
<template>
  <div>
    <button @click="sendNews">发布新闻</button>
    <button @click="sendAlert">发布警报</button>
  </div>
</template>

<script setup>
import eventBus from './mitt.js'

const sendNews = () => {
  // 在群里发新闻消息
  eventBus.emit('news', {
    title: '重大新闻!',
    content: 'Vue3发布了新版本!',
    time: new Date()
  })
}

const sendAlert = () => {
  // 在群里发警报消息
  eventBus.emit('alert', '系统出现异常,请立即处理!')
}
</script>

第三步:有人监听群消息

vue 复制代码
<!-- SubscriberComponent.vue -->
<template>
  <div>
    <div v-if="latestNews">
      <h3>最新消息:{{ latestNews.title }}</h3>
      <p>{{ latestNews.content }}</p>
    </div>
    <div v-if="alertMessage" class="alert">
      ⚠️ {{ alertMessage }}
    </div>
  </div>
</template>

<script setup>
import eventBus from './mitt.js'
import { ref, onMounted, onUnmounted } from 'vue'

const latestNews = ref(null)
const alertMessage = ref('')

const handleNews = (news) => {
  latestNews.value = news
  console.log('收到新闻:', news)
}

const handleAlert = (message) => {
  alertMessage.value = message
  console.log('收到警报:', message)
  
  // 3秒后清除警报
  setTimeout(() => {
    alertMessage.value = ''
  }, 3000)
}

// 组件挂载时开始监听
onMounted(() => {
  eventBus.on('news', handleNews)      // 监听新闻事件
  eventBus.on('alert', handleAlert)    // 监听警报事件
})

// 组件卸载时停止监听,避免内存泄漏!
onUnmounted(() => {
  eventBus.off('news', handleNews)
  eventBus.off('alert', handleAlert)
})
</script>

<style scoped>
.alert {
  color: red;
  font-weight: bold;
  padding: 10px;
  border: 1px solid red;
  border-radius: 5px;
}
</style>

第五章:深度探索事件总线与发布订阅模式

事件总线 = 现实生活中的"广播站" 🎙️

让我们用现实生活中的例子来理解这个模式:

  • 发布者:就像新闻记者,收集信息并发布
  • 订阅者:就像听众,调频到感兴趣的频道收听
  • 事件总线:就像广播电台,中转所有消息

手动实现一个简单的事件总线

想了解黑魔法是怎么工作的吗?让我们自己造一个轮子:

javascript 复制代码
// 我们自己实现的事件总线!
class SimpleEventBus {
  constructor() {
    this.events = {} // 存储所有事件和对应的回调函数
  }

  // 订阅事件 - 就像登记"我对某某话题感兴趣"
  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [] // 创建新话题的关注列表
    }
    this.events[eventName].push(callback)
  }

  // 发布事件 - 就像广播"某某话题有新闻了"
  emit(eventName, data) {
    if (this.events[eventName]) {
      this.events[eventName].forEach(callback => {
        callback(data) // 通知所有关注这个话题的人
      })
    }
  }

  // 取消订阅 - 就像说"我对这个话题没兴趣了"
  off(eventName, callback) {
    if (this.events[eventName]) {
      this.events[eventName] = this.events[eventName].filter(
        cb => cb !== callback
      )
    }
  }

  // 一次性订阅 - 就像"我只听这一次新闻"
  once(eventName, callback) {
    const onceCallback = (data) => {
      callback(data)
      this.off(eventName, onceCallback) // 执行后立即取消订阅
    }
    this.on(eventName, onceCallback)
  }
}

// 使用我们自己实现的事件总线
const myEventBus = new SimpleEventBus()

// 测试一下
myEventBus.on('greeting', (name) => {
  console.log(`Hello, ${name}!`)
})

myEventBus.emit('greeting', 'World') // 输出: Hello, World!

第六章:通信方式大比拼 🥊

通信方式 适用场景 优点 缺点
Props/Emits 父子组件通信 简单直观,Vue官方推荐 跨层级传递麻烦
Provide/Inject 跨层级组件通信 避免逐层传递,代码简洁 数据流向不够明确
事件总线 任意组件通信 完全解耦,灵活性强 难以调试,容易滥用
Vuex/Pinia 全局状态管理 状态可预测,调试工具强大 学习成本,代码量多

第七章:最佳实践与避坑指南 🚧

1. Props的设计原则

javascript 复制代码
// ✅ 好的做法
defineProps({
  userData: {
    type: Object,
    required: true,
    validator: (value) => {
      return value.id && value.name
    }
  },
  count: {
    type: Number,
    default: 0
  }
})

// ❌ 避免的做法
defineProps(['userData', 'count']) // 缺少类型校验!

2. 事件总线的正确使用姿势

javascript 复制代码
// ✅ 好的做法 - 封装成Composable
export function useEventBus() {
  const eventBus = inject('eventBus') // 通过依赖注入
  
  const emitEvent = (event, data) => {
    eventBus.emit(event, {
      timestamp: new Date(),
      component: 'MyComponent',
      ...data
    })
  }
  
  const listenEvent = (event, callback) => {
    eventBus.on(event, callback)
    
    // 自动清理
    onUnmounted(() => {
      eventBus.off(event, callback)
    })
  }
  
  return { emitEvent, listenEvent }
}

3. 内存泄漏防护

vue 复制代码
<script setup>
import { onUnmounted } from 'vue'
import eventBus from './eventBus'

// 一定要记得取消订阅!
onUnmounted(() => {
  eventBus.off('someEvent', eventHandler)
})
</script>

总结:选择合适的通信方式 🎯

通过今天的探索,我们了解了Vue组件通信的各种方式:

  • 父子亲密对话:Props/Emits
  • 祖孙心灵感应:Provide/Inject
  • 任意组件广播:事件总线
  • 全局状态管理:Vuex/Pinia(后续文章会详细介绍)

记住:没有最好的通信方式,只有最合适的场景!简单场景用Props,复杂场景考虑事件总线或状态管理。

相关推荐
mldong5 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排5 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60616 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角7 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!7 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰8 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi9 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen10 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马10 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒11 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端