JavaScript设计模式之发布-订阅模式

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

1. 现实中的发布-订阅模式

小明最近看上一套房子,到了售楼处之后才被告知,楼盘房子早已售空。好在售楼处告诉小明,不久后还有一些尾盘推出,但到底是什么时候,目前还不知道。
于是小明记下了售楼处的电话,每天都会打电话询问有没有开盘。同样,除了小明,还有小红、小强也每天都会像售楼处打电话。一个星期后,售楼处电话已厌倦了每天的电话回答相同的内容。
当然,现实中显然不是这样的,实际上是:售楼处会把意向的客户的手机号码留在售楼处,也就是小红,小明,小强的电话,当开盘时,会依次发送短信来通知他们。

2. 发布-订阅模式的作用

在刚刚的例子中,发短信通知就是一个典型的发布-订阅模式。小明,小红他们都是订阅者,他们订阅了房子开盘的消息。售楼处作为发布者,会在合适的时候依次发送给所有订阅者发送消息。
优点:

  • 购房者不用天天打电话给售楼处,在合适的时间,售楼处会作为发布者通知给所有的订阅者
  • 购房者和售楼处之前不再强耦合的在一起,当有新的购房者出现时,只需要留下手机号码,售楼处不需要关系购房者的任何情况,只需要知道谁订阅了购房消息即可。
    第一条说明发布订阅模式可以广泛引用于异步编程中,这是一种替代传递回调函数的方案。比如,订阅ajax请求的success,error事件。或者如果想在动画的每一帧完成之后做一些事情,那我们可以订阅一个事件,然后在动画的每一帧完成之后去发布这个事件。在异步变成中使用发布-订阅模式,我们就无序过多关注对象在异步运行中的内部状态,而只需要订阅感兴趣的事件发生点。
    第二条说明发布-订阅模式可以取代对象之间硬编码的通知机制,一个对象不再显试的嗲用另外一个对象的某个接口。发布-订阅模式让两个对象松耦合的联系在一起,虽然不太清除彼此的细节,但不影响他们互相通信。当有新的订阅者出现,发布者的代码不需要做任何改变,同样发布者需要改变,订阅者也不会影响。只要之前约定的事件名没有变化。
3.DOM事件

实际上,只要我们曾经在DOM节点上绑定过事件函数,那我们就曾经使用过发布-订阅模式。

document.body.addEventListener('click', function(){
    alert(1);
}, false);
document.body.click(); // 模拟用户点击

在这里我们需要监控用户点击body的动作,但我们没法预知用户点击的时间。所以我们订阅了document.body上的click事件,当body节点被点击时,body节点便会像订阅者发布这个消息。

4.自定义事件

如何一步步实现发布-订阅模式

  • 首先制定好谁是发布者
  • 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者。
  • 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数。
    另外,可以往回调函数里填入一些参数,订阅者可以接受这些参数。这是很有必要的,比如售楼处可以在发给订阅者消息里面添加房子面积,单价等信息,订阅者可以接受到消息做各自的处理
 var salesOffices = {}  // 定义售楼处

    salesOffices.clientList = []  // 缓存列表,存放订阅者的回调函数

    salesOffices.listen = function (fn) {  // 增加订阅者
        this.clientList.push(fn) // 订阅的消息添加进缓存列表
    }

    salesOffices.trigger = function () {  // 发布消息
        for (var i = 0; i < this.clientList.length; i++) {
            var fn = this.clientList[i];
            fn.apply(this, arguments) // arguments 是发布消息时带上的参数
        }
    }

    // 接下来进行简单的测试

    salesOffices.listen(function (price, squareMeter) { // 小明
        console.log('价格' + price)
        console.log('面积' + squareMeter)
    })

    salesOffices.listen(function (price, squareMeter) { // 小红
        console.log('价格' + price)
        console.log('面积' + squareMeter)
    })

    salesOffices.trigger(200000, 88)
    salesOffices.trigger(300000, 99)

至此,我们实现了一个最简单的发布-订阅模式,但是这里有一些问题,我们看到订阅者接收到了发布者的每个消息,虽然小明只想买88平方米的房子,但是同时也接受到了99平方米的信息,所以我们需要标识一个key,让订阅者制定粤自己感兴趣的消息,改写后代码如下:

var salesOffices = {}  // 定义售楼处

    salesOffices.clientList = {}  // 缓存列表,存放订阅者的回调函数

    salesOffices.listen = function (key, fn) {  // 增加订阅者
        if (!this.clientList[key]){  // 如果还没有订阅过此类的消息,给该类消息创建一个缓存列表
            this.clientList[key] = []
        }
        this.clientList[key].push(fn) // 订阅的消息添加到缓存列表
    }

    salesOffices.trigger = function () {  // 发布消息

        var key = Array.prototype.shift.call(arguments) // 取出消息类型,也就是key
        var fns = this.clientList[key] // 取出该类型所有的回调函数集合

        if (!fns || fns.length === 0) return false; // 没有订阅过该消息就返回

        for (var i = 0; i < fns.length; i++) {
            var fn = fns[i]
            fn.apply(this, arguments)
        }
    }


    salesOffices.listen('m88', function (price) {
        console.log(price);
    })

    salesOffices.listen('m99', function (price) {
        console.log(price);
    })

    salesOffices.trigger('m88', 200000)
    salesOffices.trigger('m99', 300000)

很明显,订阅者可以只订阅自己感兴趣的事件了。

5.发布-订阅模式的通用实现

小明又想去其他售楼处买房,那么就需要重新写一个salesOffices,所以需要做如下改进,把发布订阅的功能提取出来,单独放在一个对象内:


    var event = {
        clientList: {},
        listen: function (key, fn) {
            if (!this.clientList[key]){
                this.clientList[key] = []
            }
            this.clientList[key].push(fn)
        },
        trigger: function () {
            var key = Array.prototype.shift.call(arguments) // 取出消息类型,也就是key
            var fns = this.clientList[key] // 取出该类型所有的回调函数集合

            if (!fns || fns.length === 0) return false; // 没有订阅过该消息就返回

            for (var i = 0; i < fns.length; i++) {
                var fn = fns[i]
                fn.apply(this, arguments)
            }
        }
    }

    // 定义一个installEvent函数,这个函数可以给所有对象都动态安装发布-订阅功能:
    var installEvent = function (obj) {
        for (var i in event) {
            obj[i] = event[i]
        }
    }

    var salesOffices = {}

    installEvent(salesOffices)

    salesOffices.listen('ss88', function (price) {
        console.log(price);
    })

    salesOffices.listen('ss99', function (price) {
        console.log(price)
    })

    salesOffices.listen('ss88', 200000)
    salesOffices.listen('ss99', 300000)
6. 取消订阅的事件

有时候我们有这种需求,小明突然不想买房了,为了避免售楼处推送来的信息,可以取消之前订阅的事件

 event.remove = function (key, fn) {
        var fns = this.clientList[key]

        if (!fns) return false; // 如果key对应得消息没有被人订阅,则返回
        if (!fn){
            fns && (fns.length = 0) // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
        }else {
            for (var i = fns.length - 1; i > 0; i--) { // 反向遍历订阅的回调函数列表
                var _fn = fns[i]
                if (_fn === fn){
                    fns.splice(i, 1) // 删除对应得回调函数
                }
            }
        }

    }

    var salesOffices = {}

    installEvent(salesOffices)

    salesOffices.listen('ss88', fn1 = function (price) {
        console.log(price);
    })

    salesOffices.listen('ss99', fn2 = function (price) {
        console.log(price)
    })

    salesOffices.remove('ss88', fn1)
    salesOffices.trigger('ss88', 200000)
7.真实的例子--网站登录

加入我们正在开发一个网站商场,网站里面有header,nav,购物车等各模块。这几个模块的渲染有一个共同的前提条件,就是必须先用ajax异步请求获取用户的信息。
至于ajax请求什么时候能成功返回用户信息,这点我们没法确定。更重要的一点是,我们不知道除了购物车,nav,header之外,还有哪些地方需要用到用户信息。如果他们和用户之间信息产生了强耦合,比如下面这样的形式:

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

现在登录模块是我们负责编写的,我们必须还了解header模块设置头像的方法叫setAvatar,购物车刷新的方法叫refresh.等等各种方法,如果后面需要加上收货地址的模块

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

这时候,又要重构代码。
当我们用发布订阅模式重写后,对用户感兴趣的业务模块将自定订阅登录成功的消息事件。登录成功后,登录模块只需要发布登录信息,而业务方接受消息之后,可以自己做各自的业务处理。

$.ajax('http://xxx.xx.com/login',function(data){
    login.trigger('loginsuccess', data)
})

各模块监听登录成功的消息

var header = (function () {
        login.listen('loginsuccess', function (data) {
            header.setAvator(data.avator)
        });
        return {
            setAvator: function(data) {
                console.log('设置header的头像')
            }
        }
    })()

这时候,如果添加一个刷新收货地址的行为,就可以让收货地址模块开发人员去订阅登录成功这个事件

    var adderss = (function () {
        login.listen('loginsuccess', function (data) {
            adderss.refresh(data)
        });
        return {
            refresh: function (data) {
                console.log('刷新地址')
            }
        }
    })()
8. 全局的发布-订阅对象

用一个全局的Event对象来实现,订阅者不需要了解消息来自哪个发布者,发布者也不需要消息会推送给订阅者,Event作为一个中介者的角色来吧发布者和订阅者联系到一起,代码如下:

var Event = (function () {

        var clientList = {},
            listen,
            trigger,
            remove;

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

        trigger = function() {
            var key = Array.prototype.shift.call(arguments) // 取出消息类型,也就是key
            var fns = clientList[key] // 取出该类型所有的回调函数集合

            if (!fns || fns.length === 0) return false; // 没有订阅过该消息就返回

            for (var i = 0; i < fns.length; i++) {
                var fn = fns[i]
                fn.apply(this, arguments)
            }
        }

        remove = function (key, fn) {
            var fns = clientList[key]

            if (!fns) return false; // 如果key对应得消息没有被人订阅,则返回
            if (!fn){
                fns && (fns.length = 0) // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
            }else {
                for (var i = fns.length - 1; i > 0; i--) { // 反向遍历订阅的回调函数列表
                    var _fn = fns[i]
                    if (_fn === fn){
                        fns.splice(i, 1) // 删除对应得回调函数
                    }
                }
            }
        }

        return {
            listen: listen,
            trigger: trigger,
            remove: remove
        }

    })()


    Event.listen('ss88', function (data) {
        console.log(data);
    })

    Event.trigger('ss88', 222)

你可能感兴趣的:(JavaScript设计模式之发布-订阅模式)