大话发布订阅模式

1,227 阅读4分钟

这是我参与2022首次更文挑战的第22天,活动详情查看:2022首次更文挑战

介绍

本期会向介绍一种在js中极其常用的设计模式——发布订阅模式,相信大家在很多业务当中都使用过它,它既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。

如果不了解的小伙伴也没关系,我们这里先举个例子,大家都想一夜暴富,然后就去彩票站买彩票了,我们买完后到期开奖,不知道中了没有,就要跑过去问彩票站,我中奖了没?中奖号码是多少?一个人还好,但想一夜暴富的人多之又多,每个人都去彩票站问,不管谁都会疯掉的。下一次,彩票站学乖了,让每个购票者的人把联系方式留下,开奖结果一出,通过短信把结果发给留下联系方式的购票者。最终购票者即少跑了冤枉路,彩票站又不用口干舌燥的反复对每个人说结果。这也就是发布订阅模式的便利性。

概念

发布订阅模式是定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

实现

实现传统发布订阅其实非常简单,我其实就是要准备一个对象池,当注入一种事件就要把事件名记录下来存到对象池里面,其内部是一个数组,再把刚才的事件方法添加到数组里面。

let evts = {
    evtName:[fn]
}

刚才那个就形式就订阅。然后我们可以写发布推送方法,向刚刚订阅过事件的遍历那个数组,把他们挨个执行处理出来,就实现了发布推送。当然既然有订阅那么就会出现取消订阅,同样,是找到当前的事件名,如果存在相应的方法那么就从这个数组中剔除掉。接下来,我们将通过ES5和ES6两种语法来实现出来。

ES5

var MyEvent = (function () {
    var _event = function () {
        this.clientList = {};
    };
    _event.prototype.on = function () {
        var _key = Array.prototype.shift.call(arguments);
        (this.clientList[_key] || (this.clientList[_key] = [])).push(Array.prototype.shift.call(arguments));
    }
    _event.prototype.emit = function () {
        var _key = Array.prototype.shift.call(arguments);
        if (!this.clientList[_key]) return false;
        var args = arguments;
        this.clientList[_key].forEach(function (fn) {
            fn.apply(this, args);
        })
    }
    _event.prototype.off = function (key, fn) {
        var fns = this.clientList[key] || [];
        if (fns.length === 0) return false;
        if (!fn) fns && (fns.length = 0);
        for (var i = fns.length,item; item = fns[--i];) {
            if (item === fn) fns.splice(i, 1);
        }
        return true;
    }
    return _event;
})();

ES6

class MyEvent {
    constructor() {
        this.clientList = {};
    }
    on(...args) {
        let _key = Array.prototype.shift.call(args);
        (this.clientList[_key] || (this.clientList[_key] = [])).push(Array.prototype.shift.call(args));
    }
    emit(...args) {
        let _key = Array.prototype.shift.call(args);
        if (!this.clientList[_key]) return false;
         this.clientList[_key].forEach(function (fn) {
            fn.apply(this, args);
        })
    }
    off(...args) {
        const [key, fn] = args;
        let fns = this.clientList[key] || [];
        if (fns.length === 0) return false;
        if (!fn) fns && (fns.length = 0);
        for (var i = fns.length, item; item = fns[--i];) {
            if (item === fn) fns.splice(i, 1);
        }
        return true;
    }
}

使用

let $Event = new MyEvent();
let attack1 = function (name, skill) {
    console.log(this)
    console.log(name + "收到攻击指令");
}
let attack2 = function (name, skill) {
    console.log(this)
    console.log(name + "发起" + skill);
}
let wait = function (name) {
    console.log(this)
    console.log(name + "发呆中");
}
$Event.on("attack", attack1);
$Event.on("attack", attack2);
$Event.on("wait", wait);

attack里有两个订阅者,而wait里有一个,然后我们推送两条消息试试,看看能出来几条信息。

$Event.emit("attack", "亚古兽", "小型火焰");
$Event.emit("wait", "亚古兽");

微信截图_20220207203748.png

这里可以看出attack因为里面存了两个方法,当发出推送信息后,这两个方法根据参数都做出了回应,同样,wait也是。接下来,我们康康off取消订阅能否成功。

$Event.off("attack", attack1);
$Event.off("wait");

$Event.emit("attack", "亚古兽", "普通攻击");
$Event.emit("wait", "亚古兽");

其中,attack下我们抹除了attack1,而wait我们没有传递事件,可以看MyEvent类代码如果不传事件则抹除里面全部事件。

微信截图_20220207203808.png

可以看到attack1和wait都不再做出响应。

注意

我们在vue2中最常用的bus事件总线就是利用了发布订阅,27号,vue3成为官方默认版本,就不能再用bus事件总线就是利用了发布订阅,2月7号,vue3成为官方默认版本,就不能再用bus了,我们可以尝试第三方库mitt来完成发布订阅。

import mitt from "mitt";

const bus = {};
const emitter = mitt();

bus.$on = emitter.on;
bus.$off = emitter.off;
bus.$emit = emitter.emit;

export default bus;

结语

通过以上可以发现发布订阅模式优点主要是时间上可以解耦,然后是对象之间也可以解耦。但它也不是完全没有缺点。当创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,有可能消息最后都都没发出,它会始终存在于内存中。而且,发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一起的时候,要跟踪一个bug不是件轻松的事情。总之,发布订阅在绝大部分js场景中其优雅和便利性,是利大于弊的,不管在网页还是游戏中,都可以使用它来非常棒的完成模块间通信。