遇到了才了解的发布-订阅模式

不同的语言,相同的模式

之前有个项目,需要做一个控件的订阅,大致是web将需要订阅的事件值下发给控件,然后当控件触发了订阅的事件时,通知web进行操作。

这个操作在一开始,被我认为是事件触发,后来看到发布-订阅模式,稍稍了解了这大概是它的专业术语。

在控件上有这样的模式,那么js是否也有呢?答案是肯定的,比如triggeron,比如vue上的$emit$on

其实这么解释一下,感觉原本一开始看到的发布-订阅模式一下子就清晰起来了。

生活中的发布-订阅模式

最常见的就是手机的短信。比较符合的如没有消费账单、来电提醒。有些广告这些,大概也算吧。

再比如说公司的产品售出,有些买家会留下他们的手机号,然后售货员答应他,当这个产品出新的版本时会及时通知他们。

生活中的应用非常广泛,不详细介绍了。

发布-订阅模式的作用

将上面的运营商和用户,商家和买家分别看作是一个对象,其实也可以看出,发布-订阅是一种对象间的一(发布者)对多(订阅者)的松耦合关系,当一个对象状态发生变化时,所有依赖它的其他对象都会得到状态改变的通知。

1、广泛应用于异步编程中,且替换了传递回调函数的方案

2、对象之间松耦合的相互通信

代码上的使用

源生的事件

以jq为例,下面的click便是源生的事件

$("#div").bind("click", function () {
    alert("我被点击啦~");
});

$("#div").click(); //模拟用户点击
复制代码

这里需要监控用户的点击动作,但却没法预知用户在什么时候进行操作,所以我们订阅了$("#div)(发布者)上面的点击事件,当该dom元素被点击时,它会向订阅者发布这个消息。

在代码上,我们可以随意增加或者删除订阅者。

自定义事件

很多情况下,我们会使用一些自定义事件。

//发布者对象

let com = {};
//订阅者缓存列表,为什么是对象不是数组?因为订阅者都有可以分类。

//比如买家有买手机的,有买电脑的,想要手机的总不能电脑新款发布就也给他推消息吧

com.consumlist = {};

com.on = function(key, fn) {

    //比如之前只有手机,后来有个新的客户需要订阅电脑的,那么需要新增电脑分类

    if (!this.consumlist[key]) {
        this.consumlist[key] = [];
    }

    // 把函数添加到对应key的缓存列表里

    this.consumlist[key].push(fn);

}

com.emit = function() {

    // 第一个参数是对应的类

    let key = [].shift.call(arguments),
        fns = this.consumlist[key];
        
    // 如果缓存列表里没有函数就返回false
    if (!fns || fns.length === 0) {
        return false;
    }

    // 遍历key值对应的缓存列表
    // 依次执行函数的方法
    fns.forEach(fn => {
        // arguments 发布信息是推送的参数
        fn.apply(this, arguments);
    });
};

// 测试用例
com.on('phone', () => {
    console.log('手机有新款了告诉我');
});
com.on('computer', () => {
    console.log('电脑有新款了告诉我');
});

com.emit('phone');
com.emit('computer' );
/*
手机有新款了告诉我
电脑有新款了告诉我
*/
复制代码

通用实现

比如一个客户,本来在韩国手机代理商处订阅了手机,后来又搬去了日本,那是否需要在日本再写一遍发布-订阅功能呢?有没有办法让日本和韩国的代理商都有这样的功能呢?

自然是有的。还是上面那个例子,把发布-订阅功能提取出来,放到单独的对象里。

let event = {
    consumlist: {},
    on(key, fn) {
        if (!this.consumlist[key]) {
            this.consumlist[key] = [];
        }
    this.consumlist[key].push(fn);
    },
    emit() {
        let key = [].shift.call(arguments),
            fns = this.consumlist[key];

        if (!fns || fns.length === 0) {
            return false;
        }
        fns.forEach(fn => {
            fn.apply(this, arguments);
        });
    },
    remove(key, fn) {
    // 取消订阅
        let fns = this.consumlist[key];
        // 如果key对应的消息没有被人订阅,直接返回
        if (!fns) return false;
        // 如果没有传对应函数的话
        // 默认取消key对应的所有订阅
        if (!*fn*) {
            fns && (fns.length = 0);
        } else {
        //反向遍历订阅的回调函数列表
            fns.forEach((cb, i) => {
                if (cb === fn) {
                    fns.splice(i, 1); //删除订阅者
                }
            });
        }
    }
};
// 测试用例
event.on('phone', fn1 = () => {
    console.log('手机有新款了告诉我');
});
event.on('phone', fn2 = () => {
    console.log('手机版本更新了告诉我');
});
event.remove('phone', fn1);

event.emit('phone');


/*
手机版本更新了告诉我
*/
复制代码

必须先订阅后发布吗

前面所说的都是发布-订阅模式,都是订阅者先订阅一个消息,随后才能接收到发布者发布的消息,如果顺序反过来,发布者在没有订阅它的对象时,发送的消息无疑是消失在宇宙中。

但是在某些情况下,我们需要保存这些消息,等到有对象订阅它时,重新把消息发布给订阅者。

以之前做过的一个项目为例,我们拿到菜单,然后获取用户权限后才能渲染菜单,才能跳转页面。获取用户权限是一个异步操作,当获取成功后会发布一个消息(跳转页面)。但是异步请求返回的时间很快,有可能菜单还没有渲染好,他就发布了消息(跳转页面)。vue的$nextick()可以解决这个问题(仅针对Dom渲染前的发布事件),我们可以将发布事件存放到一个$nextick()中,当获取权限完成后,因为还没有渲染菜单,订阅者(dom元素)还未被渲染完成,当菜单渲染完成后,订阅者(dom元素)渲染完成,接收到原发布信息。

总结

优点

1、时间上的解耦

2、对象之间的解耦

3、可完成更松耦合的异步编程的编写

缺点

1、创建订阅者需要消耗一定的时间和内存

2、当订阅的消息最后都未发生,这个订阅者会始终存在内存里

3、虽然弱化了对象之间的联系,但过度使用(多个发布者和订阅者嵌套在一起)时,程序难以跟踪和理解

参考

《javascript设计模式与开发实践》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值