《JavaScript设计模式与开发实践》学习笔记part4-发布订阅模式

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012863664/article/details/82560788

本篇内容主要讲述JavaScript中的发布-订阅模式

第七章 发布-订阅模式

8.1 现实中的发布-订阅模式

不论是在程序世界里还是现实生活中,发布—订阅模式的应用都非常之广泛。我们先看一个现实中的例子。
小明最近看上了一套房子,到了售楼处之后才被告知,该楼盘的房子早已售罄。好在售楼 MM 告诉小明,不久后还有一些尾盘推出,开发商正在办理相关手续,手续办好后便可以购买。 但到底是什么时候,目前还没有人能够知道。
于是小明记下了售楼处的电话,以后每天都会打电话过去询问是不是已经到了购买时间。除 了小明,还有小红、小强、小龙也会每天向售楼处咨询这个问题。一个星期过后,售楼 MM 决 定辞职,因为厌倦了每天回答 1000 个相同内容的电话。
当然现实中没有这么笨的销售公司,实际上故事是这样的:小明离开之前,把电话号码留在 了售楼处。售楼 MM 答应他,新楼盘一推出就马上发信息通知小明。小红、小强和小龙也是一 样,他们的电话号码都被记在售楼处的花名册上,新楼盘推出的时候,售楼 MM 会翻开花名册, 遍历上面的电话号码,依次发送一条短信来通知他们。

8.2 DOM事件

实际上,只要我们曾经在 DOM 节点上面绑定过事件函数,那我们就曾经使用过发布—订阅模式,来看看下面这两句简单的代码发生了什么事情:

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

在这里需要监控用户点击 document.body 的动作,但是我们没办法预知用户将在什么时候点 击。所以我们订阅 document.body 上的 click 事件,当 body 节点被点击时,body 节点便会向订阅 者发布这个消息。这很像购房的例子,购房者不知道房子什么时候开售,于是他在订阅消息后等 11 待售楼处发布消息。

8.3 自定义事件

我们现在需要通过自定义事件一步步实现发布-订阅模式,继而去实现一开始说的售楼处的例子:
1. 首先要指定好谁充当发布者(比如售楼处);
2. 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者(售楼处的花名册);
3. 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数(遍历花名册,挨个发短信)。
代码如下:

var salesOffices = {};
salesOffices.clientList = [];

salesOffices.listen = function(fn) {
    this.clientList.push(fn);
};

salesOffices.trigger = function() {
    for (var i=0, fn; fn=this.clientList[i]; i++) {
        fn.apply(this, arguments);
    }
}

salesOffices.listen(function(price, squareM) {
    console.log("价格=", price);
    console.log("面积=", squareM);
});

salesOffices.listen(function(price, squareM) {
    console.log("价格=", price);
    console.log("面积=", squareM);
});

salesOffices.trigger(200000, 88);   // 输出2次 200000万,88平方米

至此,我们已经实现了一个最简单的发布—订阅模式,但这里还存在一些问题。我们看到订 阅者接收到了发布者发布的每个消息,虽然小明只想买 88 平方米的房子,但是发布者把 110 平 方米的信息也推送给了小明,这对小明来说是不必要的困扰。所以我们有必要增加一个标示 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), // 取出消息类型
        fns = this.clientList[key];     // 取出该消息对应的回调函数集合
    if (!fns || fns.length === 0) { // 如果没有订阅该消息,则返回
        return false;
    }
    for (var i = 0, fn; fn = fns[i++];) {
        fn.apply(this, arguments); // (2) // arguments 是发布消息时附送的参数
    }
};

salesOffices.listen('squareMeter88', function(price, squareM) {
    console.log("价格=", price);
    console.log("面积=", squareM);
});

salesOffices.listen('squareMeter110', function(price, squareM) {
    console.log("价格=", price);
    console.log("面积=", squareM);
});

salesOffices.trigger( 'squareMeter88', 2000000 ); // 发布 88 平方米房子的价格
salesOffices.trigger( 'squareMeter110', 3000000 ); // 发布 110 平方米房子的价格

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

8.4 取消订阅的事件

有时候,我们也许需要取消订阅事件的功能。比如小明突然不想买房子了,为了避免继续接 收到售楼处推送过来的短信,小明需要取消之前订阅的事件。现在我们给 event 对象增加 remove 方法:

salesOffices.remove = function(key, fn) {
    var fns = this.clientList[key];
    if (!fns) {  // 如果key对应的消息没有被人订阅,则直接返回
        return false;
    }
    if (!fn) {// 没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
        fns && fns.length = 0;
    } else {
        for (var l = fns.length - 1;l >= 0;l--) {  //反向遍历订阅的回调函数列表
            var _fn = fns[l];
            if (_fn === fn) {
                fns.splice(l, 1);  // 删除订阅者的回调函数    
            }
        }
    }
};
salesOffices.listen( 'squareMeter88', fn1 = function( price ){   // 小明订阅消息  
    console.log( '价格= ' + price );
});
salesOffices.listen( 'squareMeter88', fn2 = function( price ){   // 小红订阅消息
    console.log( '价格= ' + price );
});
salesOffices.remove( 'squareMeter88', fn1 ); // 删除小明的订阅
salesOffices.trigger( 'squareMeter88', 2000000 ); // 输出:2000000

8.5 真实的例子-网站登录

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

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

现在登录模块是我们负责编写的,但我们还必须了解 header 模块里设置头像的方法叫 setAvatar、购物车模块里刷新的方法叫 refresh,这种耦合性会使程序变得僵硬,header 模块不 能随意再改变 setAvatar 的方法名,它自身的名字也不能被改为 header1、header2。 这是针对具 体实现编程的典型例子,针对具体实现编程是不被赞同的。
等到有一天,项目中又新增了一个收货地址管理的模块,这个模块本来是另一个同事所写的, 而此时你正在马来西亚度假,但是他却不得不给你打电话:“Hi,登录之后麻烦刷新一下收货地 址列表。”于是你又翻开你 3 个月前写的登录模块,在最后部分加上收货地址的刷新方法的调用。
用发布—订阅模式重写之后,对用户信息感兴趣的业务模块将自行订阅登录成功的消息事件。 当登录成功时,登录模块只需要发布登录成功的消息,而业务方接受到消息之后,就会开始进行 各自的业务处理,登录模块并不关心业务方究竟要做什么,也不想去了解它们的内部细节。改善 后的代码如下:

$.ajax('http:// xxx.com?login', function (data) { // 登录成功 
    login.trigger('loginSucc', data); // 发布登录成功的消息
})

var header = (function () { // header 模块 
    login.listen( 'loginSucc', function( data){
        header.setAvatar(data.avatar);
    });
    return {
        setAvatar: function (data) {
            console.log('设置 header 模块的头像');
        }
    }
})();
// nav 模块 
var nav = (function () {
    login.listen('loginSucc', function (data) {
        nav.setAvatar( data.avatar );
    });
    return {
        setAvatar: function (avatar) {
            console.log('设置 nav 模块的头像');
        }
    }
})();

如上所述,我们随时可以把 setAvatar 的方法名改成 setTouxiang。如果有一天在登录完成之 后,又增加一个刷新收货地址列表的行为,那么只要在收货地址模块里加上监听消息的方法即可, 而这可以让开发该模块的同事自己完成,你作为登录模块的开发者,永远不用再关心这些行为了。

8.6 模块间通信

比如现在有两个模块,a 模块里面有一个按钮,每次点击按钮之后,b 模块里的 div 中会显示 按钮的总点击次数,我们用全局发布—订阅模式完成下面的代码,使得 a 模块和 b 模块可以在保 持封装性的前提下进行通信。

<!DOCTYPE html>
<html>

<body>
    <button id="count">点我</button>
    <div id="show"></div>
</body>
<script type="text/JavaScript"> 
var a = (function(){ 
    var count = 0; 
    button.onclick = function(){ 
        Event.trigger('add', count++ ); 
    })
})(); 
var b = (function(){ 
    var div = document.getElementById( 'show' ); 
    Event.listen( 'add', function(count ){ 
        div.innerHTML = count; 
    }); 
})(); 
</script>
</html>

但在这里我们要留意另一个问题,模块之间如果用了太多的全局发布—订阅模式来通信,那 么模块与模块之间的联系就被隐藏到了背后。我们最终会搞不清楚消息来自哪个模块,或者消息 会流向哪些模块,这又会给我们的维护带来一些麻烦,也许某个模块的作用就是暴露一些接口给 其他模块调用。

猜你喜欢

转载自blog.csdn.net/u012863664/article/details/82560788
今日推荐