发布订阅模式

Chen大约 10 分钟设计模式

JavaScript中,发布订阅模式(也称为观察者模式)是一种常用的设计模式,可以让不同对象之间以一种松耦合的方式进行通信。这种模式在实际项目中具有广泛的应用,可以提高代码的可维护性和灵活性。本文将通过通俗易懂的语言,深入探讨 JavaScript 中的发布订阅模式,介绍其基本概念和用法,并通过具体的代码示例来演示如何在实际项目中应用这种设计模式。

概要是用 chatgpt 生成的 ~ 觉得如何呢 ~

1. 什么是发布订阅模式 ?

  • 发布订阅模式是一种对象间一对多的依赖关系(利用消息队列),可以让对象之间轻松通信。

    • 一对多意味着 n 个订阅者对应 1 个消息队列

羞涩难懂 ~ 简单来说就是一种便于对象间(发布者与订阅者)通信的手段或方式 ~

1.1 基本概念

让我们一起想象一下 ~

  • 有一个发布者(publisher),他的职责是维护(添加、删除)一个订阅者列表,并在特定事件发生时通知所有已注册的订阅者。

  • 有多个订阅者(subsrciber),他们的职责是注册(或制定)特定事件发生时执行特定的操作(回调函数)。

  • 有一个触发器,叫做事件(或消息),它作为发布者和订阅者之间的联结,发布者负责触发事件,并通知所有的订阅者指定事件发生,从而让订阅者执行特定操作。

怎么多出一个消息队列 ~

  • 可以这么说,消息队列就是个中介 ~

  • 订阅者既然要订阅事件,那么需要把自己想订阅的事件注册到消息队列中

  • 作为发布者,发布该事件到消息队列中,也就是该事件触发时,由消息队列统一调度(执行)订阅者注册到消息队列的代码。

1.2 基本用途

  • 发布订阅模式广泛地应用于 JavaScript 客户端编程中,所有的浏览器事件(mouseoverkeypress 等)都是使用该模式的例子。

1.3 优点

  • 松耦合

    • 对象之间解耦,它们不需要直接相互引用或依赖,从而使得系统更加灵活、可维护和可扩展。
  • 可复用性

    • 发布订阅模式可以在多个地方使用,让不同的订阅者可以订阅同一个事件,从而实现功能的复用。
  • 可扩展性

    • 发布者和订阅者可以动态地增加或删除,不影响彼此的功能,从而方便地扩展系统。
  • 解耦逻辑

    • 通过将订阅者的逻辑封装在回调函数中,可以将不同的业务逻辑分离开来,使得代码更加清晰、可维护。
  • 异步处理

    • 发布订阅模式可以方便地处理异步事件,例如在异步请求完成后通知订阅者更新 UI

2. 发布订阅模式的特征

  • 我们先定义声明消息队列 message,有需要的对象(或订阅者) obj 去订阅

    • 那么,消息队列在 JavaScript 中可以用数组模拟 [fn1,fn2,fn3]

    • 需要注意的是:对象不是主动触发,而是被动触发! 这也决定了该设计模式的优势之处 !

举个栗子来理解吧 ~

  • 小张去书店买书,去书店问:“没有”,回家。过一会再去问,没有,回家。过一会又再去问:没有,回家。

  • 相当于在实际开发场景中,频繁的调用方法(轮询)。

    • 例:使用 websocket 长连接,setInterval...
    • 缺点:网络负担大,服务器开销大。

  • 小张去书店买书,问:没有,先订阅(on),就是给书店店员联系方式,一旦有了书,店员就是发送一条消息,然后手机响了触发消息队列叫小张去买书,书买着了,联系方式不需要了。

  • 相当于在实际开发场景中,使用发布订阅模式。

    • addEventListener() 本质上就是一个发布订阅模式的应用,比如绑定点击事件,不是主动触发方法,而是状态发生变化(点击)被动触发回调函数。

3. 实现一个简单的发布订阅模式

  • 整体实现思路

    • 创建一个类 class --> Observer

    • 在这个类里创建一个消息队列 message

    • on 方法 - 用于将回调函数 fn 添加到消息队列(订阅者注册事件到这里)

    • emit 方法 - 取到 event 事件类型,根据 event 值去执行对应消息列表中的函数(发布者发布事件到消息队列,消息队列处理代码)

    • off 方法 - 可以根据 event 事件类型取消订阅(取消订阅)

3.1 搭建基本结构

简化版

/* 
	分析构造函数
	属性:消息队列
	{
		"click": [fn1, fn2, fn3],
		"xxx": [handlerA, handlerB],
		//...
	}
	能向消息队列里面添加内容 $on
	删除消息队列中的内容 $off
	触发消息队列中内容 $emit  
*/
class Observer {
	constructor() { }
	$on() { }
	$off() { }
	$emit() { }
}












 
 
 
 
 

3.2 初始化消息队列

class Observer {
	constructor() {
        this.message = {
            // "红宝书": [handlerA, handlerB]
        }
    }
	$on() { }
	$off() { }
	$emit() { }
}

 
 
 
 
 




3.3 $on 方法编写

  • 用于将回调函数 fn 添加到消息队列 this.message

实际上是type 绑定相关事件。类似于:btn.addEventListener("click",fn)

//todo...
$on(type,fn) {
   // 先判断消息队列中是否有 type 属性
   // 有 --> 直接添加到消息队列 (使用 push...)
   // 没有 --> 为 type 属性初始化一个空数组
   if(!this.message[type]) {
       this.message[type] = [];
   }
   this.messagep[type].push(fn);
}
//todo...

 
 
 
 
 
 
 
 
 

检验一下!

// 使用构造函数创建一个实例
const p1 = new Observer();
p1.$on("红宝书", handlerA);
p1.$on("红宝书", handlerB);
function handlerA() {
	console.log("handlerA");
}
function handlerB() {
	console.log("handlerB");
}

没问题!

3.4 $off 方法编写

  • 根据 event 事件类型取消订阅(取消订阅)
// todo...
$off(type, fn) {
    // 首先判断是否订阅,没有订阅直接返回或报错异常
    // 再判断是否 fn 为 空(undefined)
    // 是 --> 删除 type 全部事件 
    // 不是就仅删除这个函数方法
    if (!this.message[type]) {
        return false;
        // return new Error('没有订阅 type...');
    }
    if (!fn) {
        // delete this.message[type]
        this.message[type] = undefined;
        return;
    } else {
        // 删除 --> 过滤掉这个方法或其他方法...
        this.message[type] = this.message[type].filter(item => item !== fn);
    }
}
// todo...

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

验证(略)

3.5 $emit 方法编写

  • 获取到 event 事件类型,根据 event 值去执行对应缓存列表中的函数(发布者发布事件到调度中心,调度中心处理代码)

实际上就是去执行回调函数!

$emit(type) {
    if (!this.message[type]) {
        return false;
    }
    this.message[type].forEach(item => {
        //遍历并执行
        item();
    }
}

检验一下!

const p1 = new Observer();
p1.$on("红宝书", handlerA);
p1.$on("红宝书", handlerB);
p1.$on("红宝书", handlerC);
p1.$emit("红宝书");

是不是很简单!实践一下吧!


4. 实际场景运用

4.1 杂志订阅

  • 假设有一个发布者 paper,它发行一个日报和月刊。无论是日报还是日刊,有一个名为 jack 的订阅者都会收到通知。

  • paper 对象有一个 subscribers 属性,是一个数组,用于保存所有的订阅者。订阅的过程就是仅将订阅者放入数组。

  • 当一个事件发生时,paper 遍历这个订阅者列表,然后通知它们。通知其实就是调用订阅者对象的一个方法。因此,在订阅的过程中,订阅者需要提供一个方法给 paper 对象的subsrcibe

  • 因为这些成员方法对任何对象都是通用的,因此可以作为一个单独的、公共的对象如 publisher 提取出来,可以通过混元模式将它们复制到任何对象当中,比如 paper 对象。

// 做法很多 类 构造函数 对象(我们使用)
// 发布者是一个对象 我们写一个具有通用功能的发布者
var publisher = {
    // 用于保存所有的订阅者
    subscribers: {
    	any: []
        // 默认事件类型 any
    	  // "红宝书": [handlerA, handlerB]
	},
	// 类似于 Observer 中的 $on 
	subscribe: function(type, fn) {
        type = type || "any";
        if(typeof this.subscribers[type] === 'undefined') {
            this.subscribers[type] = []
        }
        this.subscribers[type].push(fn);
    }
}
  • paper 对象也可以提供 unsubscribe() 方法,它用于将订阅者从数组者移除。还有一个方法 publish(),用于调用订阅者的方法。
  • 定义辅助方法 visitSubscribers
{
    // todo...
    // 辅助方法 visitSubscribers 辅助 publish 和 unsubscribe 也可以不使用辅助方法 类似于 3.
    visitSubscribers: function(action, arg, type) {
        type = type || "any";
        // action 行为 publish/unsubscribe
        // arg 参数  publication/fn
        // type “要绑定的事件”
        var subscribers = this.subscribers[type];
        // subscribers 订阅 type 的订阅者数组
        var max = subscribers.length;
        for(var i = 0; i < max; i++) {
            if(action === 'publish') {
                // 依次调用订阅者的方法
                // arg 为 publication
                subscribers[i](arg);
            } else {
                // action 为 unsubscribe
                if(subscribers[i] === arg) {
                    // arg 为 fn 
                    //找到这个要剔除的方法
                    subscribers.splice(i,1);
                }
            }
        }
    }
    // todo...
}
  • publishunsubscribe 方法编写,借助辅助方法
{
    // todo...
    publish: function(publication,type) {
        // 调用辅助方法
        this.visitSubscribers("publish",publication,type);
    },
   	unsubscribe: function(type,fn) {
        this.visitSubscribers("unsubscribe",fn,type);
    }
    // todo...
}
  • 使用混元模式(简单理解就是将任意多数量的对象中复制属性。然后将它们混在一起组成一个新对象),通过复制通过发布者的方法将如 paper 对象转变为发布者。
function makePublisher(obj) {
    for(var i in publisher) {
        //原型属性不需要 只需要方法
        if(publisher.hasOwnProperty(i) && typeof publisher[i] === 'function') {
            obj[i] = publisher[i];
        }
    }
    // 注意要添加 subscribers 属性,没有会报错
    obj.subscribers = { any: [] };
}
  • 实现 paper 对象,发布日报和月刊,并变成发布者
var paper = {
    daily: function() {
    	this.publish("日报来咯");
	},
    monthly: function() {
        this.publish("月刊来咯","monthly");
    }
}

makePublisher(paper);
  • 声明订阅者 jack ,用于订阅 paper
var jack = {
    // 有两个方法
    drinkCoffee: function(paper) {
        console.log('喝着咖啡,' + paper);
    },
    sundayPreNap: function(monthly) {
        console.log('睡觉前,' + monthly);
    }
}

// jack 订阅 paper
paper.subscribe('monthly', jack.sundayPreNap);
/*  相当于
	this.subscribers = {
		"monthly": [ sundayPreNap ]
	}	
*/
paper.subscribe(undefined, jack.drinkCoffee); 
/*  相当于
	this.subscribers = {
		"any": [ drinkCoffee ],
		"monthly": [ sundayPreNap ],
	}	
*/
  • 触发事件
paper.monthly();
// monthly 事件发生时触发
// 会调用 publish 方法 --> 相当于执行 sundayPreNap
// "睡觉前,月刊来咯"
paper.daily();
// any 事件发生时触发 (默认事件)
// 会调用 publish 方法 --> 相当于执行 drinkCoffee
// "喝着咖啡,日刊来咯"
// 这里的 publish 方法我们不是直接调用的,而是间接调用

检验一下

  • 此时涉及到的对象都是松耦合的,而且不在修改代码的前提下,我们可以给 paper 添加更多的订阅者,同时 jack 可以在任何时候取消订阅!
  • 取消订阅
  • 现在我们把 jack 也变成一个发布者,在博客和微博等平台上,人人都可以是发布者!
makePublisher(jack);
jack.blog = function(msg) {
    this.publish(msg);
}
  • 现在 jack可以在博客上发布文章了,让 paper 订阅一下 jack 的博客以便审核内容,提供一个方法 readBlog()
paper.readBlog = function(blog) {
    console.log("审核博客内容为:" + blog);
}
//订阅 默认事件 any
jack.subscribe(undefined, paper.readBlog);
//发布博客文章
jack.blog("发布订阅模式");

太棒了!居然看完了!

小结

发布订阅模式是一个用于对象间通信的设计模式,通信方式更加灵活、可维护和可扩展,像在 VueReact 等前端框架中,组件之间的通信,其中就有使用到发布订阅模式,具体可以看看 pubsub-js,一个深受欢迎且较为成熟的库 ~

希望本文对你有所帮助 !