发布订阅模式

164 阅读4分钟

发布订阅模式

比如:pubsub-j

含义说明

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

作用

  1. 支持简单的广播通信,当对象状态发生改变时,会自动通知已经订阅过的对象。
  2. 可以应用在异步编程中,替代回调函数,可以订阅ajax之后的事件,只需要订阅自己需要的部分 (那么ajax掉用发布之后订间的就可以拿到消息了) (不需要关心对象在异步运行时候的状态)
  3. 对象之间的松耦合,两个对象之间都互相不了解彼此;但是,不影响通信,当有新的订阅者出现的时候,发布的代码无需要改变同样发布的代码改变,只要之前约定的事件的名称没有改变,也不影响订阅。
  4. vue react之间实现跨组件之间的传值

缺点

  1. 创建订阅者需要消耗一定的时间和内存。
  2. 虽然可以弱化对象之间的联系,如果过度使用的话,反而使代码不好理解及代码不好维护等等。

生活中的实例

比如小红最近在淘宝网上看上一双鞋子,但是呢联系到卖家后,才发现这双鞋卖光了,但是小红对这双鞋又非常喜欢,所以呢联系卖家,问卖家什么时候有货,卖家告诉她, 要等一个星期后才有货,卖家告诉小红,要是你喜欢的话,你可以收藏我们的店铺,等有货的时候再通知你,所以小红收藏了此店铺,但与此同时,小明,小花等也喜欢这双鞋,也收藏了该店铺;等来货的时候就依次会通知他们;

如何实现发布--订阅模式?

  1. 首先要想好谁是发布者(比如上面的卖家)。
  2. 然后给发布者添加一个缓存列表,用于存放回调函数来通知订阅者(比如上面的买家收藏了卖家的店铺,卖家通过收藏了该店铺的个列表名单)。
  3. 最后就是发布消息,发布者遍历这个缓存列表,依次触发里面存放的订阅者回调函数。
    // 定义发布者
    let shoeObj = {};
    // 有一个列表用来存放函数
    shoeObj.list = [];

    // 增加订阅者
    shoeObj.listen = function(key, fn){
        // 判断是否存在
        if (!this.list[key]) {
            this.list[key] = [];
        }
        this.list[key].push(fn);
    }
    // 发布消息
    shoeObj.trigger = function(){
        // 取出key
        let key = Array.prototype.shift.call(arguments);
        let fns = this.list[key];
        // 遍历这个数据, 然后执行这个函数
        // for(let i = 0, fn;fn = this.list[i++];) {
        //     fn();
        // }
        if (!fns || fns.length == 0) {
            return;
        }
        for(let i = 0;i < fns.length; i++) {
            let fn = fns[i];
            fn.apply(this, arguments);
        }
    }

    // 运用
    // 订阅
    shoeObj.listen('red', function(color, size) {
        console.log('======1=======')
        console.log(`颜色是${color}`);
        console.log(`大小是${size}`);
    });
    shoeObj.listen('blue', function(color, size) {
        console.log('======2=======')
        console.log(`颜色是${color}`);
        console.log(`大小是${size}`);
    });
    console.log(shoeObj, '==shoeObj=')
    // 发布
    shoeObj.trigger('red', '红色', 32);
    shoeObj.trigger('blue', '蓝色', 20);

总结:

const pubsub = (function() {
    let list = {}, listen, trigger, remove;

    listen = function(key, fn) {
        if (!list[key]) {
            list[key] = [];
        }
        list[key].push(fn);
    }

    trigger = function(){
        let key = Array.prototype.shift.call(arguments);
        let fns = list[key];

        if (!fns || fns.length === 0) {
            return false;
        }
        for(let i = 0; i < fns.length; i++) {
            let fn = fns[i];
            fn.apply(this, arguments);
        }
    }

    remove = function(key, fn) {
        let fns = list[key];
        if (!fns) {
            fns && (fns.length = 0);
            return false;
        }

        for(let i = 0; i < fns.length; i++) {
            let _fn = fns[i];
            if (_fn === fn) {
                fns.splice(i, 1);
            }
        }
    }
    return {
        listen,
        trigger,
        remove,
    };
})();

实例1:

<button class="count">点击我</button>
<div class="show-count"></div>
// 发布
function publish(){
    let count = 0;
    let button = document.querySelector('.count');
    button.addEventListener('click', function(e) {
        pubsub.trigger('add', count++)
    });
}
// 订阅
function subscribe(){
    let showCount = document.querySelector('.show-count');
    pubsub.listen('add', function(count) {
        showCount.textContent = count;
    });
}

function init(){
    publish();
    subscribe();
}
init();

改进异步操作中的强耦合

业务场景

假如正在开发一个商城网站,网站里有header头部、 nav导航、消息列表、购物车等模块。这几个模块的染有一个共同的前提条件,就是必须先用ajax异步请求获取用户的登录信息。这是很正常的,比如用户的名字和头像要显示在header模块里,而这两个字段都来自用户登录后返回的信息

login.succ(function(data){
    //设置header模块的头像
    header.setAvatar(data.avatar);
    //设置导航模块的头像
    nav.setAvatar( data.avatar );
    // 刷新消息列表
    message.refresh();
    //刷新购物车列表
    cart.refresh();
})

强耦合

现在必须了解header模块里设置头像的方法叫setAvatar、购物车模块里刷新的方法叫refresh,这种耦合性会使程序变得僵硬,header模块不能随意再改变setAvatar的方法名,它自身的名字也不能被改为header1、header2。

  • 等到有一天,项目中又新增了一个收货地址管理的模块,在最后部分加上这行代码
login.succ(function(data){
    // ...
    address.refresh();
})

发布订阅模式,实现低耦合

用发布-订阅模式重写之后,对用户信息感兴趣的业务模块将自行订阅登录成功的消息事件。当登录成功时,登录模块只需要发布登录成功的消息,而业务方接受到消息之后,就会开始进行各自的业务处理,登录模块并不关心业务方究竟要做什么,也不想去了解它们的内部细节。

$.ajax(' http://xx.com?login', function(data) {
    //登录成功消息
    //发布登录成功的
    login.trigger('loginSucc' ,data);
});
  • 各模块监听登录成功的消息
// header模块
let header = (function(){
    login.listen('loginSucc', function(){
        //设置header模块的头像
        header.setAvatar(data.avatar);
    });
    return {
        setAvatar: function(){
            console.log('设置header模块的头像');
        },
    };
})();