|

JavaScript常用设计模式:发布订阅模式

前端页面开发中经常会用到设计模式,其中最常见的是单例模式、工厂模式和发布订阅模式。如大名鼎鼎的 JQuery 就是单例模式,整个页面中只有 $ 一个全局变量作为 JQuery 的引用。本文暂不讨论单例模式,而是介绍发布订阅模式,这个模式在 Vue 的中也有使用,例如在 Vue 中初始化的全局总线就是一个发布订阅模式的应用。

本篇将主要整理和实现一个发布订阅模型单例。

发布订阅模式

发布订阅模式是一种在开发中经常会使用到的设计模式,一般由订阅者订阅话题,然后由发布者发布话题,以便实现一种异步的事件回调。

后端中大名鼎鼎的 Redis 数据库也参考了该模式,另外,在物联网开发中注明的MQTT协议,则完全实现了该模式。

微信公众号也基于这种模式,比如:

  • 只有公众号关注者才能收到公众号的推送,关注者即订阅者;
  • 公众号值负责推送,不关心订阅者是谁,只要订阅者存在,就发送推文;
  • 订阅者不需要轮询,也不用查阅是否有数据更新,当公众号需要推送消息时,订阅者就会收到消息;
  • 关注了公众号的人也可以取消关注,即订阅者可以取消订阅;

用公众号这个模型来看,发布者与订阅者直接其实是不需要互相关心的,订阅者只负责订阅,发布者只负责发布,当发布者发布消息时,订阅者就会收到消息。

我们把发布者抽象为总线,熟悉硬件开发的朋友应该比较熟悉这个名词比如 I2C 总线,设备与主机之间也是通过地址来今天通讯。

总线中的发布者即总线本身,只不过这个是可以编程的。总线需要实现至少三个方法:

  • on,订阅事件,关联事件回调;
  • emit,触发事件,相当于发布消息,
  • off,取消订阅,实际是取消事件关联函数。

我们来实现它:

function Bus () {
    let topicHandles = {};

    // 添加监听
    function addListener(topic, func) {
        if (!topicHandles[topic]) {
            topicHandles[topic] = [];
        }
        topicHandles[topic].push(func);
    }

    // 移除监听
    function removeListener(topic, func) {
        if (!topicHandles[topic]) return;
        let index = topicHandles[topic].indexOf(func);
        if (index > -1) {
            topicHandles[topic].splice(index, 1);
        }
    }

    // 触发监听回调
    function trigTopicHandle(topic, ...params) {
        if (!topicHandles[topic]) return;
        for (let func in topicHandles[topic]) {
            topicHandles[topic][func](...params);
        }
    }

    return {
        on: function (topic, func) {
            if (typeof func !== "function") {
                throw "func must be a function";
            }
            addListener(topic, func);
        },
        emit: function (topic, ...parmas) {
            trigTopicHandle(topic, ...parmas);
        },
        off: function (topic, func) {
            if (typeof func !== "function") {
                throw "func must be a function";
            }
            removeListener(topic, func);
        }
    }
}

上面代码实现的 Bus 既是发布者,也是订阅者。

代码中 topicHandles 是一个对象,存放的是 topic 与回调函数的映射,在代码中定义了一些变量和函数,全部使用的是局部定义,没有在 this 上绑定,因为我们不希望开发者访问其中的数据,topicHandles 应该只能由 Bus 对象自己来维护。

实例化一个总线对象:

var bus = Bus();

订阅

当我们需要订阅一个 topic 时,可以这样实现:

bus.on("deviceOnline", function () {
    console.log("device online");
})

上面的回调函数是一个匿名函数,有时候,我们需要可以取消某个函数的订阅关系,这么时候需要使用一个具名函数,所以也可以这样写:

function onDeviceOnline () {
    console.log("device online");
}

如果需要撤掉某个函数与话题的回调关系,可以使用 off 方法来解除:

bus.off("deviceOnline", onDeviceOnline);

发布

发布也很简单:

bus.emit("deviceOnline");
// 控制台输出:
// device online

功能扩展

很多时候,我们希望只订阅一次话题,所以增加一个 once 方法;同时,我们也希望能清楚某个话题的所有回调,于是再增加 clear 方法。

function Bus () {
    let topicHandles = {};

    // 添加监听
    function addListener(topic, func, once = false) {
        if (!topicHandles[topic]) {
            topicHandles[topic] = [];
        }
        topicHandles[topic].push({
            handle: func,
            once: once
        });
    }

    // 移除监听
    function removeListener(topic, func = null) {
        if (!topicHandles[topic]) return;
        if (func !== null) {
            for (let i in topicHandles[topic]) {
                if (topicHandles[topic][i].handle === func) {
                    topicHandles[topic].splice(i, 1);
                    return;
                }
            }
        }
        else {
            delete topicHandles[topic];
        }
    }

    // 触发监听回调
    function trigTopicHandle(topic, ...params) {
        if (!topicHandles[topic]) return;
        for (let i in topicHandles[topic]) {
            topicHandles[topic][i].handle(...params);
            if (topicHandles[topic][i].once) {
                topicHandles[topic].splice(i, 1);
                i--;
            }
        }
    }

    return {
        on: function (topic, func, once = false) {
            if (typeof func !== "function") {
                throw "func must be a function";
            }
            addListener(topic, func, once);
        },
        once: function (topic, func) {
            this.on(topic, func, true);
        },
        emit: function (topic, ...parmas) {
            trigTopicHandle(topic, ...parmas);
        },
        off: function (topic, func) {
            if (typeof func !== "function") {
                throw "func must be a function";
            }
            removeListener(topic, func);
        },
        clear: function (topic) {
            removeListener(topic);
        }
    }
}

测试:

var bus = Bus();
bus.once("hello", function () {
    console.log("hello");
})

bus.emit("hello");
bus.emit("hello");
// 输出
// hello

可以看到,只有一个 hello 输出,这是符合预期的。到目前为止,这是一个有效的发布订阅管理器,基于这个函数我们可以在任何地方来异步的推送消息。

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注