前端最常见的设计模式——发布订阅篇
sinye56 2024-10-22 16:25 6 浏览 0 评论
概念
有的人说:发布—订阅模式又叫观察者模式,但是我不这么认为,感觉它们之间其实还是有区别的。
观察者模式:
- 结构: 在观察者模式中,通常包含两个主要角色 — 观察者(Observers)和被观察者(Subject)。
- 关系: 观察者直接订阅被观察者。被观察者维护一个观察者列表,并在状态变化时通知所有观察者。
- 通知方式: 被观察者主动向观察者推送信息,通常是调用观察者的方法来进行通知。
发布-订阅模式:
- 结构: 在发布-订阅模式中,有一个中介者(通常称为“事件总线”或“消息队列”)来管理订阅者和发布者之间的关系。
- 关系: 发布者和订阅者不直接耦合,它们通过中介者进行通信。发布者将消息发送到中介者,然后中介者将消息传递给所有订阅者。
- 通知方式: 订阅者通过向中介者注册感兴趣的事件或主题,中介者在接收到消息后负责将消息分发给所有订阅者。
关键区别:
- 直接耦合 vs 间接耦合: 观察者模式中,观察者和被观察者直接耦合;而发布-订阅模式中,发布者和订阅者通过中介者间接耦合。
- 通信方式: 观察者模式中,通常是被观察者直接通知观察者;发布-订阅模式中,通过中介者进行通信,发布者只需向中介者发布消息,而不需要直接通知订阅者。
观察者模式更适合简单的一对多通信,而发布-订阅模式则更适用于松散耦合的场景,尤其是在复杂系统中需要处理多个事件和消息的情况。
发布订阅模式: 订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Topic),当发布者(Publisher)发布该事件(Publish topic)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。
在 JavaScript 开发中,我们一般用事件模型来替代传统的发布—订阅模式。
作用
可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。
可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。
从架构上来看,无论是 MVC 还是 MVVM,都少不了发布—订阅模式的参与,而且 JavaScript 本身也是一门基于事件驱动的语言。
简单的理解
DOM 事件
下边这段代码就是利用了发布订阅模式。
document.getElementById("myBtn").addEventListener("click", function(){
alert("Hello World!");
});
简易发布订阅
// on 是订阅 emit 是发布
let e = {
// 存订阅
_callback: [],
on(callback) {
// 订阅一件事 当这件事发生的时候 触发对应的函数
// 订阅 就是将函数放到数组中
this._callback.push(callback);
},
emit(value) {
this._callback.forEach(method => {
method(value);
});
}
};
// 订阅
e.on(function (value) {
console.log(value + ":张三的订阅");
});
// 订阅
e.on(function (value) {
console.log(value + ":李四的订阅");
});
// 订阅
e.on(function (value) {
console.log(value + ":王五的订阅");
});
// 发布
e.emit('发布')
自定义事件
let salesOffices = { // 定义售楼处
clientList: {}, // 缓存列表,存放订阅者的回调函数
listen(key, fn) {
if (!this.clientList[key]) { // 如果还没有订阅过此类消息,给该类消息创建一个缓存列表
this.clientList[key] = [];
}
this.clientList[key].push(fn); // 订阅的消息添加进消息缓存列表
},
trigger() { // 发布消息
let key = Array.prototype.shift.call(arguments) // 取出消息类型
let fns = this.clientList[key]; // 取出该消息对应的回调函数集合
if (!fns || fns.length === 0) { // 如果没有订阅该消息,则返回
return false;
}
for(let fn of fns){ // (2) // arguments是发布消息时附送的参数
fn.apply(this, arguments);
}
}
};
// 例子
salesOffices.listen('squareMeter88', price => console.log(`价格= ${price}`));
salesOffices.listen('squareMeter110', price => console.log(`价格= ${price}`));
salesOffices.trigger('squareMeter88', 2000000);
salesOffices.trigger('squareMeter110', 3000000);
通用的实现
通用的一种封装,实现了订阅、发布、取消。
const event = {
clientList: [],
listen: function( key, fn ){
if (!this.clientList[key] ){
this.clientList[key] = [];
}
this.clientList[key].push(fn); // 订阅的消息添加进缓存列表
},
trigger: function(){
const key = Array.prototype.shift.call( arguments ) // (1);
const fns = this.clientList[ key ]
if (!fns || fns.length === 0){ // 如果没有绑定对应的消息
return false;
}
for(let i = 0, fn; fn = fns[i++];){
fn.apply(this, arguments); // (2) // arguments是trigger时带上的参数
}
},
remove: function(key, fn){
let fns = this.clientList[key];
if (!fns){ // 如果key对应的消息没有被人订阅,则直接返回
return false;
}
if (!fn){ // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
fns && (fns.length = 0);
} else {
for (let l = fns.length -1; l >=0; l--){ // 反向遍历订阅的回调函数列表
let _fn = fns[l];
if (_fn === fn){
fns.splice(l, 1); // 删除订阅者的回调函数
}
}
}
}
};
const installEvent = function( obj ){
obj = { ...obj, ...event }
};
let salesOffices = {};
installEvent(salesOffices);
salesOffices.listen( 'squareMeter88', fn1 = function(price){ // 小明订阅消息
console.log('价格= ' + price);
});
salesOffices.listen( 'squareMeter100', fn2 = function(price){ // 小红订阅消息
console.log('价格= ' + price );
});
salesOffices.remove('squareMeter88', fn1); // 删除小明的订阅
salesOffices.trigger('squareMeter88', 2000000); // 输出:2000000
salesOffices.trigger('squareMeter100', 3000000); // 输出:3000000
谁用了?
在前端开发中,有许多库和框架使用了发布-订阅模式或提供了类似的机制,以便实现组件通信、事件处理、状态管理等功能。
以下是一些常见的前端库和框架,它们使用了发布-订阅模式或相关的设计模式:
PubSubJS
PubSubJS 是一个实现发布-订阅模式的 JavaScript 库。
PubSubJS 是一个独立的 JavaScript 模块,它提供了发布-订阅模式的实现。它可以在任何 JavaScript 环境中使用,包括浏览器和 Node.js。
以下是 src/pubsub.js 文件的核心部分的简要解释:
- 模块模式: 代码使用了 JavaScript 的模块模式,通过 IIFE(立即调用函数表达式)封装了整个库。
(function (root, factory) {
// ...
}(this, function () {
// ...
}));
- 核心对象: 主要的 PubSub 对象,并且有一些私有的辅助函数和变量。
var PubSub = {};
// 发布
PubSub.publish = function( message, data ){};
PubSub.publishSync = function( message, data ){};
// 订阅
PubSub.subscribe = function( message, func ){};
// 订阅所有
PubSub.subscribeAll = function( func ){};
// 允许只订阅一次的消息
PubSub.subscribeOnce = function( message, func ){};
// 用于清除所有订阅
PubSub.clearAllSubscriptions = function clearAllSubscriptions(){};
PubSub.clearSubscriptions = function clearSubscriptions(topic){};
PubSub.countSubscriptions = function countSubscriptions(topic){};
PubSub.getSubscriptions = function getSubscriptions(topic){};
// 取消订阅
PubSub.unsubscribe = function(value){};
var messages = {};
Vue
Vue 提供了一个简单的事件系统,通过 vm.$emit 发布事件,vm.$on 订阅事件。这种机制类似于发布-订阅模式,允许组件之间进行松散耦合的通信。
在 Vue 中使用发布-订阅模式的例子:
- 使用 EventBus: 你可以创建一个简单的 EventBus,用于在不同组件之间进行通信。
// EventBus.js
import Vue from 'vue';
export const EventBus = new Vue();
// ComponentA.vue
import { EventBus } from './EventBus';
export default {
methods: {
sendMessage() {
EventBus.$emit('message', 'Hello from ComponentA!');
}
}
}
// ComponentB.vue
import { EventBus } from './EventBus';
export default {
created() {
EventBus.$on('message', message => {
console.log('Received message in ComponentB:', message);
});
}
}
- 使用 provide/inject: 如果你在一个深层嵌套的组件结构中,可以使用 provide 和 inject 来提供一个全局的事件总线。
// EventBus.js
import Vue from 'vue';
export const EventBus = new Vue();
// App.vue
import { EventBus } from './EventBus';
export default {
provide: {
eventBus: EventBus
}
}
// ComponentA.vue
export default {
inject: ['eventBus'],
methods: {
sendMessage() {
this.eventBus.$emit('message', 'Hello from ComponentA!');
}
}
}
// ComponentB.vue
export default {
inject: ['eventBus'],
created() {
this.eventBus.$on('message', message => {
console.log('Received message in ComponentB:', message);
});
}
}
React
在 React 中,并没有像 Vue 那样直接提供发布-订阅模式的特定实现。React 更倾向于使用单向数据流和通过 props 和回调函数进行组件通信。
然而,如果你需要在 React 中实现发布-订阅模式,你可以借助全局状态管理库(如Redux)、或使用第三方发布订阅库 PubSubJS 、或使用 React 的 Context API 。
以下是一个使用 React Context API 来实现发布-订阅模式的简单例子:
// EventBusContext.js
import { createContext, useContext } from 'react';
const EventBusContext = createContext();
export const useEventBus = () => {
return useContext(EventBusContext);
};
export const EventBusProvider = ({ children }) => {
const listeners = {};
const subscribe = (eventName, callback) => {
if (!listeners[eventName]) {
listeners[eventName] = [];
}
listeners[eventName].push(callback);
return () => {
listeners[eventName] = listeners[eventName].filter(cb => cb !== callback);
};
};
const publish = (eventName, data) => {
if (listeners[eventName]) {
listeners[eventName].forEach(callback => {
callback(data);
});
}
};
const value = {
subscribe,
publish
};
return (
<EventBusContext.Provider value={value}>
{children}
</EventBusContext.Provider>
);
};
// ComponentA.js
import React from 'react';
import { useEventBus } from './EventBusContext';
const ComponentA = () => {
const eventBus = useEventBus();
const sendMessage = () => {
eventBus.publish('message', 'Hello from ComponentA!');
};
return (
<div>
<button onClick={sendMessage}>Send Message</button>
</div>
);
};
export default ComponentA;
// ComponentB.js
import React, { useEffect } from 'react';
import { useEventBus } from './EventBusContext';
const ComponentB = () => {
const eventBus = useEventBus();
useEffect(() => {
const unsubscribe = eventBus.subscribe('message', message => {
console.log('Received message in ComponentB:', message);
});
return () => {
unsubscribe();
};
}, [eventBus]);
return <div>ComponentB</div>;
};
export default ComponentB;
Redux
在 Redux 中,并没有直接提供经典的发布-订阅模式,Redux 引入了一种单向数据流的架构,其中状态的变化通过派发(dispatch)动作来进行管理。
Redux 中有一些机制可以达到一些类似发布-订阅的效果:
- 中间件(Middleware): Redux 中的中间件可以截获派发的动作,进行一些额外的操作,然后将动作传递给下一个中间件或 Redux store。
const loggerMiddleware = store => next => action => {
console.log('Dispatching action:', action);
return next(action);
};
const store = createStore(
rootReducer,
applyMiddleware(loggerMiddleware)
);
- Store.subscribe 方法: Redux 的 store 对象提供了一个 subscribe 方法,该方法接受一个回调函数,每当状态发生变化时都会被调用。
const store = createStore(rootReducer);
const unsubscribe = store.subscribe(() => {
console.log('State changed:', store.getState());
});
// 当不再需要订阅时,调用 unsubscribe 取消订阅
unsubscribe();
jQuery
jQuery 中的事件系统是一个经典的发布-订阅模式的实现。通过 jQuery 的事件系统,你可以轻松地订阅和发布自定义事件,以及处理浏览器原生事件。
jQuery 提供了一种简单而强大的方式来实现发布-订阅模式,使得在应用中不同模块之间的通信更加灵活和可维护。
在现代的前端开发中,虽然现代框架提供了更先进的状态管理和组件通信机制,但对于一些小型项目或不需要引入复杂状态管理的场景,jQuery 事件系统仍然是一个实用的工具。
- 事件绑定:
// 订阅自定义事件
$(document).on('customEvent', function(event, data) {
console.log('Custom event received with data:', data);
});
// 发布自定义事件
$(document).trigger('customEvent', 'Hello, jQuery!');
- 事件命名空间:
// 订阅带有命名空间的事件
$(document).on('customEvent.myNamespace', function(event, data) {
console.log('Custom event with namespace received with data:', data);
});
// 发布带有命名空间的事件
$(document).trigger('customEvent.myNamespace', 'Hello, jQuery with namespace!');
- 一次性事件处理:
// 订阅一次性事件
$(document).one('customEvent', function(event, data) {
console.log('One-time custom event received with data:', data);
});
// 发布一次性事件
$(document).trigger('customEvent', 'Hello, jQuery one-time!');
- 解除事件绑定:
// 解除事件绑定
$(document).off('customEvent');
// 将不再接收 customEvent 事件
$(document).trigger('customEvent', 'This will not be logged.');
最后
发布-订阅模式在前端开发中有许多实际应用场景,它提供了一种松散耦合的机制,允许不同模块或组件之间进行灵活的通信。
相关推荐
- Linux两种光驱自动挂载的方法
-
环境:CentOS6.4西昆云服务器方式一修改fstab文件/etc/fstab是系统保存文件系统信息?静态文件,每一行描述一个文件系统;系统每次启动会读取此文件信息以确定需要挂载哪些文件系统。参...
- linux系统运维,挂载和分区概念太难?在虚机下操作一次全掌握
-
虚拟机的好处就是可以模拟和学习生产环境的一切操作,假如我们还不熟悉磁盘操作,那先在虚机环境下多操作几次。这次来练习下硬盘扩容操作。虚拟机环境:centos8vm11linux设备命名规则在linux中...
- Linux 挂载 NFS 外部存储 (mount 和 /etc/fstab)
-
mount:手工挂载,下次重启需再重新挂载,操作命令:mount-tnfs-ooptionsserver:/remote/export/local/directory上面命令中,本地目录...
- 在Linux中如何设置自动挂载特定文件系统(示例)
-
Linux...
- Linux环境中的绑定挂载(bind mount)
-
简介:Linux中的mount命令是一个特殊的指令,主要用于挂载文件目录。而绑定挂载(bindmount)命令更为特别。mount的bind选项将第一个目录克隆到第二个。一个目录中的改变将会在...
- Linux挂载CIFS共享 临时挂载 1. 首先
-
如何解决服务器存储空间不足的问题?大家好,欢迎回来。在上一期视频中,我为大家介绍了如何利用Linux挂载来扩容服务器存储空间。这一期视频,我将以Linux为例,教大家如何进行扩容。群辉使用的是Linu...
- Linux 硬盘挂载(服务器重启自动挂载)
-
1、先查看目前机器上有几块硬盘,及已挂载磁盘:fdisk-l能够查看到当前主机上已连接上的磁盘,以及已经分割的磁盘分区。(下面以/dev/vdb磁盘进行分区、挂载为例,挂载点设置为/data)df...
- linux 挂载磁盘
-
在Linux中挂载硬盘的步骤如下:...
- 笨小猪教您Linux磁盘挂载
-
本教程针对Linux系统比较熟悉或者想学习Linux基础的用户朋友,本教程操作起来比较傻瓜式,跟着步骤就会操作,本文使用的工具是XShell同时多多注意空格(文中会有提示)。【问答】什么是磁盘挂载?答...
- Linux 磁盘挂载和docker安装命令
-
本篇给大家介绍Linux磁盘挂载和docker安装的相关内容,Linux服务器的操作是一个手熟的过程,一些不常用的命令隔断时间就忘记了,熟话说好记性不如烂笔头,还需在平时的工作中多练习记录。...
- Linux设置开机自动挂载分区
-
有时候,我们在安装完Linux系统之后,可能在使用过程中添加硬盘或者分区进行使用,这时候就需要手动把磁盘分区挂载到某个路径,但是开机之后就会消失,需要重新挂载,非常麻烦,那么我们应该如何设置开机自动挂...
- 在linux挂载一个新硬盘的完整步骤
-
以下是在Linux中挂载新原始磁盘的完整步骤,包括分区、创建文件系统以及使用UUID在/etc/fstab中启动时挂载磁盘:将新的原始磁盘连接到Linux系统并打开电源。运行以下命令,...
- Linux系统如何挂载exFAT分区
-
简介:Linux系统中不能像Windows系统那样自动识别加载新设备,需要手动识别,手动加载。Linux中一切皆文件。文件通过一个很大的文件树来组织,文件树的根目录是:/,从根目开始录逐级展开。这些文...
- Linux系统挂载硬盘
-
fdisk-l查看可挂载的磁盘都有哪些df-h查看已经挂载的磁盘...
- WSL2发布,如何在Win10中挂载Linux文件系统
-
WSL2是最新版本的架构,它为Windows子系统提供支持,使其能够在Windows上运行ELF64Linux二进制文件。通过最近的更新,它允许使用Linux文件系统访问存储在硬盘中的文件。如果你...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle忘记用户名密码 (59)
- oracle11gr2安装教程 (55)
- mybatis调用oracle存储过程 (67)
- oracle spool的用法 (57)
- oracle asm 磁盘管理 (67)
- 前端 设计模式 (64)
- 前端面试vue (56)
- linux格式化 (55)
- linux图形界面 (62)
- linux文件压缩 (75)
- Linux设置权限 (53)
- linux服务器配置 (62)
- mysql安装linux (71)
- linux启动命令 (59)
- 查看linux磁盘 (72)
- linux用户组 (74)
- linux多线程 (70)
- linux设备驱动 (53)
- linux自启动 (59)
- linux网络命令 (55)
- linux传文件 (60)
- linux打包文件 (58)
- linux查看数据库 (61)
- linux获取ip (64)
- 关闭防火墙linux (53)