浅谈设计模式 之 【发布订阅模式】

298 阅读8分钟

前言

在软件开发的世界里,设计模式如同古老的智慧,经久不衰,它们是解决常见问题的最佳实践,是开发者们在长期的实践中总结出的宝贵经验。对于我们前端开发者而言,掌握一些重要的设计模式,不仅能够提升代码的可维护性和扩展性,还能在面对复杂的前端架构时游刃有余。本文主要是围绕设计模式的的概念、分类和设计原则展开,浅谈一下经典的发布订阅模式和该模式的实现以及在 Vue 框架中的一些典型应用,闲话不多说,我们直接进入正题...

一、设计模式

1.1 概念

设计模式是软件开发中用于解决常见问题的通用、可复用的解决方案。它们是经过验证的最佳实践,可以帮助开发者更有效地解决特定类型的问题,提高代码的可维护性、可扩展性和可重用性。设计模式通常并不是指具体的代码实现,而是一种思想和方法论,可以在不同的编程语言和环境中实现。

1.2 设计模式的分类

设计模式通常分为三大类:

  1. 创建型模式(Creational Patterns)

    • 这些模式关注对象的创建过程,试图以适当的方式创建对象,以避免高成本或复杂性的问题。
    • 常见的创建型模式包括:工厂方法模式、抽象工厂模式、单例模式、建造者模式和原型模式。
  2. 结构型模式(Structural Patterns)

    • 这些模式关注类和对象的组合,以形成更大的结构。
    • 常见的结构型模式包括:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式和享元模式。
  3. 行为型模式(Behavioral Patterns)

    • 这些模式关注对象之间的责任分配和交互,特别是算法和对象之间的职责分配。
    • 常见的行为型模式包括:发布订阅模式、观察者模式、策略模式、模板方法模式、命令模式、迭代器模式、中介者模式、备忘录模式、解释器模式和访问者模式。

1.3 设计模式的重要原则

  1. 单一职责原则(SRP) :一个类或模块应该有且只有一个引起它变化的原因。这意味着每个组件都应该专注于做一件事,并且做好它。

  2. 开放封闭原则(OCP) :软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。这意味着我们应该通过增加新的代码来扩展功能,而不是修改现有的代码。

  3. 里氏替换原则(LSP) :子类应该能够替换其基类并且不会引起程序的错误。这保证了继承的正确使用,使得代码更加健壮。

  4. 接口隔离原则(ISP) :客户端不应该依赖它不需要的接口。这意味着我们应该定义小而专的接口,而不是大而全的接口。

  5. 依赖倒置原则(DIP) :高层模块不应该依赖低层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。

二、发布订阅模式

在前端开发中,发布订阅模式(Publish-Subscribe Pattern)是一种极为重要的设计模式。它如同一个高效的通信系统,让信息的发布者和订阅者之间解耦,从而实现灵活的事件处理机制。

2.1 发布订阅模式的工作原理

发布订阅模式中,存在一个事件通道(Event Channel),它充当着中介的角色。发布者(Publisher)将事件发布到这个通道,而订阅者(Subscriber)则订阅自己感兴趣的事件。当事件发生时,通道会将事件通知给所有订阅者,订阅者则根据事件执行相应的处理逻辑。

2.2 发布订阅模式的优点

  1. 解耦:发布者和订阅者之间没有直接的依赖关系,它们通过事件通道进行通信,这大大降低了系统的耦合度。

  2. 灵活性:由于事件通道的存在,系统可以轻松地添加新的发布者和订阅者,而无需修改现有的代码。

  3. 可扩展性:发布订阅模式支持事件的多对多关系,一个事件可以被多个订阅者处理,一个订阅者也可以订阅多个事件,这为系统的扩展提供了极大的便利。

2.3 编码实现一个发布订阅模式

// 创建一个事件通信类
class EventChannel {
    constructor() {
        // 用于存储订阅者的回调函数
        this.subscribes = {};
    }

    // 订阅事件
    subscribe(event, callback) {
        if(!this.subscribers[evevt]) {
            this.subscribers[event] = [];
        }
        this.subscribers[event].push(callback);
    }
    
    // 取消订阅事件
    unsubscribe(event, callback) {
        if(this.subscribers[event]) {
            this.subscribers[event] = this.subscribers[event].filter(cb => cb !== callback);
        }
    }
    
    // 发布事件
    publish(event, data) {
        if(this.subscribers[event]) {
            this.subscribers[event].forEach(callback => callback(data));
        }
    }
}

// 创建一个事件通道实例
const evevtChannel = new EventChannel();

// 定义一个订阅者回调函数
const subscriberCallback = (data) => {
    console.log('Recieved data:', data);
};

// 订阅一个事件(exampleEvent 为事件名,subscribeCallback 为该事件名对应的事件回调,也就是订阅者订阅了这个事件之后我们需要这个事件后续执行什么逻辑,我们也可以传递参数给这个被订阅的事件)
eventChannel.subscribe('exampleEvent', subscriberCallback);

// 发布一个事件
eventChannel.publish('exampleEvent', { message: 'Hello World!!!' });

// 取消订阅事件
eventChannel.unsubscribe('exampleEvent', subscriberCallback);

// 再次发布此事件,但是此时没有订阅者接收
eventChannel.publish('exampleEvent', { message: 'this will not be received!!!'});

现在简要解释一下上述代码:

EventChannel 类:

  1. subscribers: 一个对象,用于存储事件名称和对应的回调函数数组。
  2. subscribe(event, callback): 订阅事件的方法,将回调函数添加到对应事件的数组中。
  3. unsubscribe(event, callback): 取消订阅事件的方法,从对应的事件数组中移除回调函数。
  4. publish(event, data): 发布事件的方法,触发所有订阅该事件的函数,并传递数据。

订阅者和发布者

  1. subscriberCallback: 一个简单的回调函数,用于处理接收到的数据。
  2. eventChannel.subscribe('exampleEvent', subscriberCallback): 订阅名为 exampleEvent 的事件。
  3. eventChannel.publish('exampleEvent', { message: 'Hello World!!!' }):发布名为 exampleEvent 的事件,并传递数据 { message: 'Hello World!!!' }。 4.eventChannel.unsubscribe('exampleEvent', subscriberCallback):取消订阅 exampleEvent 事件。

通过上述实现,我们可以看到发布订阅模式如何在前端开发中实现事件的解耦和灵活处理,我们还可以根据需要扩展这个实现,添加更多功能,比如一键清除事件订阅者只能订阅一次命名空间,优先级等功能,以下我们一一实现,代码如下:

一键清除事件

class EventChannel {
  constructor() {
    this.subscribers = {};
  }

  subscribe(event, callback) {
    if (!this.subscribers[event]) {
      this.subscribers[event] = [];
    }
    this.subscribers[event].push(callback);
  }

  unsubscribe(event, callback) {
    if (this.subscribers[event]) {
      this.subscribers[event] = this.subscribers[event].filter(cb => cb !== callback);
    }
  }

  publish(event, data) {
    if (this.subscribers[event]) {
      this.subscribers[event].forEach(callback => callback(data));
    }
  }

  // 核心代码
  clearEvent(event) {
    if (this.subscribers[event]) {
      this.subscribers[event] = [];
    }
  }
}

const eventChannel = new EventChannel();

const subscriberCallback = (data) => {
  console.log('Received data:', data);
};

eventChannel.subscribe('exampleEvent', subscriberCallback);
eventChannel.publish('exampleEvent', { message: 'Hello, world!' });

eventChannel.clearEvent('exampleEvent');
eventChannel.publish('exampleEvent', { message: 'This will not be received' });

订阅者只能订阅一次:

class EventChannel {
  constructor() {
    this.subscribers = {};
  }

  // 核心代码
  subscribeOnce(event, callback) {
    const onceCallback = (data) => {
      callback(data);
      this.unsubscribe(event, onceCallback);
    };
    this.subscribe(event, onceCallback);
  }

  subscribe(event, callback) {
    if (!this.subscribers[event]) {
      this.subscribers[event] = [];
    }
    this.subscribers[event].push(callback);
  }

  unsubscribe(event, callback) {
    if (this.subscribers[event]) {
      this.subscribers[event] = this.subscribers[event].filter(cb => cb !== callback);
    }
  }

  publish(event, data) {
    if (this.subscribers[event]) {
      this.subscribers[event].forEach(callback => callback(data));
    }
  }
}

const eventChannel = new EventChannel();

const subscriberCallback = (data) => {
  console.log('Received data:', data);
};

eventChannel.subscribeOnce('exampleEvent', subscriberCallback);
eventChannel.publish('exampleEvent', { message: 'Hello, world!' });
eventChannel.publish('exampleEvent', { message: 'This will not be received' });

命名空间:在发布订阅模式中,命名空间可以用来为事件添加前缀,从而避免不同模块或功能的事件名称冲突。例如,如果两个模块都有名为 update 的事件,通过使用命名空间,我们可以将它们分别命名为 moduleA:update 和 moduleB:update,从而避免冲突。

class EventChannel {
  constructor() {
    this.subscribers = {};
  }

  subscribe(namespace, event, callback) {
    const fullEvent = `${namespace}:${event}`;
    if (!this.subscribers[fullEvent]) {
      this.subscribers[fullEvent] = [];
    }
    this.subscribers[fullEvent].push(callback);
  }

  unsubscribe(namespace, event, callback) {
    const fullEvent = `${namespace}:${event}`;
    if (this.subscribers[fullEvent]) {
      this.subscribers[fullEvent] = this.subscribers[fullEvent].filter(cb => cb !== callback);
    }
  }

  publish(namespace, event, data) {
    const fullEvent = `${namespace}:${event}`;
    if (this.subscribers[fullEvent]) {
      this.subscribers[fullEvent].forEach(callback => callback(data));
    }
  }
}

const eventChannel = new EventChannel();

const subscriberCallback = (data) => {
  console.log('Received data:', data);
};

eventChannel.subscribe('namespace1', 'exampleEvent', subscriberCallback);
eventChannel.publish('namespace1', 'exampleEvent', { message: 'Hello from namespace1!' });

eventChannel.subscribe('namespace2', 'exampleEvent', subscriberCallback);
eventChannel.publish('namespace2', 'exampleEvent', { message: 'Hello from namespace2!' });

优先级

class EventChannel {
  constructor() {
    this.subscribers = {};
  }

  subscribe(event, callback, priority = 0) {
    if (!this.subscribers[event]) {
      this.subscribers[event] = [];
    }
    this.subscribers[event].push({ callback, priority });
    // 核心代码
    this.subscribers[event].sort((a, b) => b.priority - a.priority);
  }

  unsubscribe(event, callback) {
    if (this.subscribers[event]) {
      this.subscribers[event] = this.subscribers[event].filter(sub => sub.callback !== callback);
    }
  }

  publish(event, data) {
    if (this.subscribers[event]) {
      this.subscribers[event].forEach(({ callback }) => callback(data));
    }
  }
}

const eventChannel = new EventChannel();

const subscriberCallback1 = (data) => {
  console.log('Received data (priority 1):', data);
};

const subscriberCallback2 = (data) => {
  console.log('Received data (priority 2):', data);
};

eventChannel.subscribe('exampleEvent', subscriberCallback1, 1);
eventChannel.subscribe('exampleEvent', subscriberCallback2, 2);
eventChannel.publish('exampleEvent', { message: 'Hello, world!' });

三、发布订阅模式在 Vue 中的典型应用场景

3.1 Vue实例作为事件总线

// main.js
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

// 创建一个全局事件总线
app.config.globalProperties.$eventBus = new Vue();

app.mount('#app');

// 组件A ==》 发布事件
<template>
  <button @click="emitEvent">Click me</button>
</template>

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

const { proxy } = getCurrentInstance();

const emitEvent = () => {
  proxy.$eventBus.$emit('custom-event', { message: 'Hello from Component A!' });
};
</script>

// 组件B ==> 订阅事件
<template>
  <div></div>
</template>

<script setup>
import { getCurrentInstance, onMounted } from 'vue';

const { proxy } = getCurrentInstance();

onMounted(() => {
  proxy.$eventBus.$on('custom-event', (data) => {
    console.log('Received data from Component A:', data);
  });
});
</script>

3.2 父子组件通信

// 父组件 ==》 订阅事件
<template>
  <child-component @custom-event="handleEvent"></child-component>
</template>

<script setup>
import ChildComponent from './ChildComponent.vue';

const handleEvent = (data) => {
  console.log('Received data from child:', data);
};
</script>

//子组件 ==》 发布事件
<template>
  <button @click="emitEvent">Click me</button>
</template>

<script setup>
const emitEvent = () => {
  emit('custom-event', { message: 'Hello from child!' });
};
</script>

3.3 Vuex状态管理

// store.js
import { createStore } from 'vuex';

export default createStore({
  state: {
    message: ''
  },
  mutations: {
    updateMessage(state, payload) {
      state.message = payload.message;
    }
  },
  actions: {
    updateMessage({ commit }, payload) {
      commit('updateMessage', payload);
    }
  }
});

// main.js
import { createApp } from 'vue';
import App from './App.vue';
import store from './store';

const app = createApp(App);
app.use(store);
app.mount('#app');

// 组件
<template>
  <div>{{ message }}</div>
  <button @click="updateMessage">Update Message</button>
</template>

<script setup>
import { useStore } from 'vuex';
import { computed } from 'vue';

const store = useStore();
const message = computed(() => store.state.message);

const updateMessage = () => {
  store.dispatch('updateMessage', { message: 'Hello from Vuex!' });
};
</script>

3.4 自定义事件(事件总线)

// 组件A ==》 发布事件
<template>
  <button @click="emitCustomEvent">Click me</button>
</template>

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

const { proxy } = getCurrentInstance();

const emitCustomEvent = () => {
  proxy.$eventBus.$emit('custom-event', { message: 'Hello from Component A!' });
};
</script>

//组件B ==》 订阅事件
<template>
  <div></div>
</template>

<script setup>
import { getCurrentInstance, onMounted } from 'vue';

const { proxy } = getCurrentInstance();

onMounted(() => {
  proxy.$eventBus.$on('custom-event', (data) => {
    console.log('Received data from Component A:', data);
  });
});
</script>

3.5 全局事件总线 :

// main.js
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

// 创建一个全局事件总线
app.config.globalProperties.$eventBus = new Vue();

app.mount('#app');

// 组件A ==》 发布事件
<template>
  <button @click="emitGlobalEvent">Click me</button>
</template>

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

const { proxy } = getCurrentInstance();

const emitGlobalEvent = () => {
  proxy.$eventBus.$emit('global-event', { message: 'Hello from Component A!' });
};
</script>

// 组件B ==> 订阅事件
<template>
  <div></div>
</template>

<script setup>
import { getCurrentInstance, onMounted } from 'vue';

const { proxy } = getCurrentInstance();

onMounted(() => {
  proxy.$eventBus.$on('global-event', (data) => {
    console.log('Received data from Component A:', data);
  });
});
</script>

结语: 鄙人的知识点总结就这么多了,感谢观看!!!

微信图片_20240728003924.jpg