Vue消息订阅与发布

一、什么是发布-订阅模式?

发布-订阅模式 是一种对象间通信的设计模式,包含三个核心角色:

  1. 发布者(Publisher):发出消息(事件)
  2. 订阅者(Subscriber):监听并处理消息
  3. 事件中心(Event Channel):中介,负责消息的注册与分发

类比理解

想象一个"广播电台":

  • 电台(事件中心)负责广播
  • 主持人(发布者)说:"现在是北京时间 8 点整"
  • 听众(订阅者)听到后,各自执行动作(起床、煮咖啡、打卡)

✅ 发布者无需知道谁在听,订阅者也无需知道谁在说,实现解耦

二、核心概念:订阅、发布、取消订阅

操作 方法 说明
订阅 on(event, callback) 注册事件监听器
发布 emit(event, ...args) 触发事件,传递数据
取消订阅 off(event, callback) 移除监听,防止内存泄漏

三、在 Vue 中的实现方式

1. Vue 2:使用 Vue 实例作为事件总线

Vue 2 的实例本身实现了事件接口($on$emit$off),可直接用作事件中心。

(1) 创建 EventBus
javascript 复制代码
// utils/eventBus.js
import Vue from 'vue'

// 创建一个 Vue 实例作为全局事件中心
const EventBus = new Vue()

export default EventBus
(2) 订阅消息(在组件中)
html 复制代码
<!-- NotificationBar.vue -->
<script>
import EventBus from '@/utils/eventBus'

export default {
  data() {
    return {
      message: ''
    }
  },
  created() {
    // 订阅 'show-notification' 事件
    EventBus.$on('show-notification', (msg, type) => {
      this.message = `[${type}] ${msg}`
      this.show()
    })
  },
  beforeDestroy() {
    // 组件销毁时取消订阅,避免内存泄漏
    EventBus.$off('show-notification')
  },
  methods: {
    show() {
      // 显示通知
      console.log(this.message)
    }
  }
}
</script>
(3) 发布消息
html 复制代码
<!-- LoginButton.vue -->
<script>
import EventBus from '@/utils/eventBus'

export default {
  methods: {
    login() {
      // 发布登录成功事件
      EventBus.$emit('user-logged-in', { id: 1, name: 'Alice' })
      
      // 发布全局通知
      EventBus.$emit('show-notification', '登录成功!', 'success')
    }
  }
}
</script>

✅ 实现了跨组件通信,但需手动管理生命周期


2. Vue 3:使用 mitt 库(推荐)

Vue 3 移除了实例上的 $on$off 方法,因此不能再使用 Vue 实例作为事件总线。

(1) 安装 mitt
bash 复制代码
npm install mitt

mitt 是一个超轻量(<200B)的事件发射器,API 简洁,完美替代。

(2) 创建全局事件中心
javascript 复制代码
// utils/eventBus.js
import mitt from 'mitt'

// 创建事件中心
const EventBus = mitt()

export default EventBus
(3) 在 Vue 3 组件中使用
html 复制代码
<!-- NotificationBar.vue -->
<script setup>
import { onMounted, onUnmounted } from 'vue'
import EventBus from '@/utils/eventBus'

// 订阅事件
const handleNotification = (data) => {
  console.log(`[通知] ${data.type}: ${data.msg}`)
}

onMounted(() => {
  EventBus.on('show-notification', handleNotification)
})

// 取消订阅
onUnmounted(() => {
  EventBus.off('show-notification', handleNotification)
})
</script>
html 复制代码
<!-- LoginButton.vue -->
<script setup>
import EventBus from '@/utils/eventBus'

function login() {
  // 发布事件
  EventBus.emit('user-logged-in', { id: 1, name: 'Bob' })
  EventBus.emit('show-notification', {
    msg: '登录成功!',
    type: 'success'
  })
}
</script>

<template>
  <button @click="login">登录</button>
</template>

mitt 支持 TypeScript,API 简洁:onemitoffclear

四、手写一个简易的 Event Hub

为了理解原理,我们来实现一个极简版的事件中心:

javascript 复制代码
// utils/EventHub.js
class EventHub {
  constructor() {
    this.events = {}
  }

  // 订阅
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = []
    }
    this.events[event].push(callback)
  }

  // 发布
  emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(...args))
    }
  }

  // 取消订阅
  off(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(cb => cb !== callback)
    }
  }

  // 清除所有
  clear(event) {
    if (event) {
      delete this.events[event]
    } else {
      this.events = {}
    }
  }
}

export default new EventHub()

✅ 核心就是维护一个事件-回调函数的映射表。

五、发布-订阅 vs 其他通信方式

通信方式 方向 耦合度 适用场景
发布-订阅 任意 广播通知、解耦组件
props / emit 父↔子 紧密相关的父子组件
Pinia 任意 共享状态、复杂逻辑
Provide/Inject 祖→孙 主题、配置传递

📌 选择建议

  • 状态共享 → Pinia
  • 简单通知 → mitt
  • 祖先传值 → provide/inject

六、最佳实践与注意事项

✅ 1. 使用语义化事件名

javascript 复制代码
// ❌ 避免模糊命名
EventBus.emit('click')

// ✅ 使用前缀和语义化命名
EventBus.emit('user:login-success', user)
EventBus.emit('cart:item-added', item)

✅ 2. 务必取消订阅

javascript 复制代码
// Vue 2
beforeDestroy() {
  EventBus.$off('event-name')
}

// Vue 3 Composition API
onUnmounted(() => {
  EventBus.off('event-name', handler)
})

❌ 忘记 off 会导致内存泄漏重复触发

✅ 3. 避免过度使用

  • 不要用事件总线代替状态管理
  • 复杂状态同步 → 使用 Pinia
  • 事件过多 → 考虑模块化事件中心

✅ 4. TypeScript 支持

TypeScript 复制代码
// types/events.ts
type Events = {
  'user:login': (user: User) => void
  'cart:update': (count: number) => void
  'app:loading': (status: boolean) => void
}

// utils/eventBus.ts
import mitt from 'mitt'
export const useEventBus = () => mitt<Events>()

✅ 类型安全,IDE 自动提示。

七、现代替代方案:Pinia + Actions

对于大多数场景,Pinia 是更优解:

TypeScript 复制代码
// stores/appStore.ts
import { defineStore } from 'pinia'

export const useAppStore = defineStore('app', {
  state: () => ({
    user: null,
    cartCount: 0,
    isLoading: false
  }),
  actions: {
    login(user) {
      this.user = user
      // 其他组件自动响应
    },
    addToCart(item) {
      this.cartCount++
    },
    setLoading(status) {
      this.isLoading = status
    }
  }
})
html 复制代码
<!-- 任何组件 -->
<script setup>
import { useAppStore } from '@/stores/appStore'

const appStore = useAppStore()

// 直接调用 action
appStore.login(userData)
</script>

✅ 响应式、可调试、支持 SSR。

八、总结

核心点 说明
本质 解耦的事件驱动通信
角色 发布者、订阅者、事件中心
Vue 2 new Vue() 作为 EventBus
Vue 3 使用 mitt
优点 灵活、解耦、轻量
缺点 难以追踪、易内存泄漏
推荐 简单通知用 mitt,状态用 Pinia

📌 一句话总结
发布-订阅是"消息广播",而 Pinia 是"状态中心"------根据场景选择合适的工具

九、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

相关推荐
下一站丶8 小时前
【JavaScript性能优化实战】
开发语言·javascript·性能优化
GIS好难学8 小时前
Three.js 粒子特效实战③:粒子重组效果
开发语言·前端·javascript
申阳8 小时前
Day 2:我用了2小时,上线了一个还算凑合的博客站点
前端·后端·程序员
刺客_Andy8 小时前
React 第四十七节 Router 中useLinkClickHandler使用详解及开发注意事项案例
前端·javascript·react.js
爱分享的鱼鱼9 小时前
Java实践之路(一):记账程序
前端·后端
爱编码的傅同学9 小时前
【HTML教学】成为前端大师的入门教学
前端·html
爱看书的小沐9 小时前
【小沐杂货铺】基于Three.js绘制三维管道Pipe(WebGL、vue、react)
javascript·vue.js·webgl·three.js·管道·pipe·三维管道
w2sfot9 小时前
如何将React自定义语法转化为标准JavaScript语法?
javascript·react
秋枫969 小时前
使用React Bootstrap搭建网页界面
前端·react.js·bootstrap