原生JavaScript实现订阅/发布模式


###1 订阅/发布模式说明
什么是订阅/发布模式呢?
举个简单的例子。我们在开发网站常常有这样的一种情况。就是用户登录过的主界面和没登录过
的主界面显示是不一样的,而没有使用订阅/发布模式的话。我们很可能会写出下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//登录成功情况下
login.succ(function( data ){
header.setAvatar( data.avatar);
nav.setAvatar( data.avatar );
message.refresh();
cart.refresh();
address.refresh();
});

//登录失败情况下
login.fail(function( data ){
header.setAvatar( data.avatar);
nav.setAvatar( data.avatar );
message.refresh();
cart.refresh();
address.refresh();
});

/*这两种情况都有相应的处理逻辑,假如现在又出现了新的模块的话。我们又要在上述的两种情况下
加入逻辑代码,十分不利于维护。*/

###2 订阅/发布模式的实践
如果我们利用订阅/发布模式的话,整个流程就十分清晰了。
模式解释:发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象
的状态发生改变时,所有依赖于它的对象都将得到通知。在 JavaScript 开发中,我们一般用事件模
型来替代传统的发布—订阅模式。
流程如下:

  • 创建发布者对象,它具有订阅者的对象数组(我们网页中的各个模块)。以及相应的发布通知这些对象执行相应方法。
  • 创建订阅者。它具有接收到发布信息的执行方法(模块初始化方法)。
  • 发布者接收到通知(登录成功)。
  • 对所有订阅者对象数组进行通知。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//实践代码

//创建发布者对象,它具有订阅者的对象数组
//(我们网页中的各个模块)。以及相应的发布通知这些对象执行相应方法。
var login = {}; // 定义发布者。
login.modules = []; // 缓存列表,存放模块订阅者的回调函数
login.listen = function( fn ){ // 增加订阅者
this.clientList.push( fn ); // 订阅的消息添加进缓存列表
};
login.trigger = function(){ // 发布消息
for( var i = 0, fn; fn = this.modules[ i++ ]; ){
fn.apply( this, arguments ); // (2) // arguments 是发布消息时带上的参数
}
};



$.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 模块的头像' );
}
}
})();
var nav = (function(){ // nav 模块
login.listen( 'loginSucc', function( data ){
nav.setAvatar( data.avatar );
});
return {
setAvatar: function( avatar ){
console.log( '设置 nav 模块的头像' );
}
}
})();

###2.1改进
以上就是一个简单的订阅/发布的实现。但上述代码还有问题,比如我们给每一个发布者都赋予了订阅者的参数列表是一种
资源浪费。我们的订阅者至少需要知道发布者的名字才能接受到信息。硬编码十分明显。接下来。我们可以使用一个全局的订阅、
发布对象,抽象出一个“中介者”的对象。他负责把订阅者以及发布者隔离开。
以一个售楼处以及客户的关系作为例子。同样在程序中,发布—订阅模式可以用一个全局的 Event 对象来实现,订阅者不需要
了解消息来自哪个发布者,发布者也不知道消息会推送给哪些订阅者,Event 作为一个类似“中介者”的角色,把订阅者和发布
者联系起来。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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),
fns = clientList[key];
if (!fns || fns.length === 0) {
return false;
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments);
}
};
//订阅者取消订阅
remove = function (key, fn) {
var fns = clientList[key];
if (!fns) {
return false;
}

if (!fn) {
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);
}
}
}
};
//暴露的公共接口
return {
listen: listen,
trigger: trigger,
remove: remove
}
})();
//小红订阅消息
Event.listen('squareMeter88', function (price) {
// 小红订阅消息,小红想要订阅88平方米的楼房价格。
//我们发现并不需要小红这个对象传入,只需要传入要监听的对象,以及回调函数即可
console.log('价格= ' + price); // 这里为小红接收到信息所做出的业务操作。输出:'价格=2000000'
});
//发布者发布消息
Event.trigger('squareMeter88', 2000000); // 售楼处发布88平方米的楼房价格消息。

到这里为止。一个比较完善的模式流程就差不多搞定了。

###2.2 界面实践
接下来我们用一个关于界面的示例来形象说明如何使用它。
需求:比如现在有两个模块,a 模块里面有一个按钮,每次点击按钮之后,b 模块里的 div 中会显示
按钮的总点击次数,我们用全局发布—订阅模式完成下面的代码,使得 a 模块和 b 模块可以在保
持封装性的前提下进行通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<html>

<body>
<!--a模块触发点击点击事件-->
<button id="count">点我</button>
<!--b模块,订阅点击事情,并作出反应-->
<div id="show"></div>
</body>
<script>

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),
fns = clientList[key];
if (!fns || fns.length === 0) {
return false;
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments);
}
};
//订阅者取消订阅
remove = function (key, fn) {
var fns = clientList[key];
if (!fns) {
return false;
}

if (!fn) {
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);
}
}
}
};
//暴露的公共接口
return {
listen: listen,
trigger: trigger,
remove: remove
}
})();

var a = (function () {
var count = 0;
var button = document.getElementById("count");
button.onclick = function () {
console.log("发布点击add消息,并随需要传入参数");
Event.trigger("add", count++)
}
})();
var b = (function () {
var div = document.getElementById("show");
console.log("订阅点击add消息,并获得a模块的参数");
Event.listen("add", function (count) {
div.innerHTML = count;

})
})();


</script>

</html>

本文小结:
本章我们学习了发布—订阅模式,也就是常说的观察者模式。发布—订阅模式在实际开发中非
常有用。

  • 它的优点:一为时间上的解耦,二为对象之间的解耦。它的应用非常广泛,既可以用在异步编程
    中,也可以帮助我们完成更松耦合的代码编写。发布—订阅模式还可以用来帮助实现一些别的设计
    模式,比如中介者模式。从架构上来看,无论是 MVC 还是 MVVM,都少不了发布—订阅模式的参与
    ,而且 JavaScript 本身也是一门基于事件驱动的语言。
  • 它的缺点:创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息
    最后都未发生,但这个订阅者会始终存在于内存中。另外,发布—订阅模式虽然可以弱化对象之间的
    联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪
    维护和理解。特别是有多个发布者和订阅者嵌套到一起的时候,要跟踪一个 bug 不是件轻松的事情。

#3 小发现
在编码的时候发现了原生JavaScript的onclick和click的区别,现在放入这里供大家参考
区别:

  • 原生javascript的click在w3c里边的阐述是DOM button对象,也是html DOM click() 方法,
    可模拟在按钮上的一次鼠标单击。button 对象代表 HTML 文档中的一个按钮。button元素没有默
    认的行为,但是必须有一个 onclick 事件句柄以便使用。
  • onclick是一个事件,Event对象 。支持该事件的 JavaScript 对象:button, document,
    checkbox, link, radio, reset, submitHTML DOM Event 对象,代表事件的状态,比如事
    件在其中发生的元素、键盘按键的状态、鼠标的位置、鼠标按钮的状态。事件通常与函数结合使用,函数
    不会在事件发生前被执行!
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <html>
    <head>
    <script type="text/javascript">
    function clickButton()
    {
    document.getElementById('button1').click()
    }
    function alertMsg()
    {
    alert("Button 1 was clicked!")
    }
    </script>
    </head>
    <body onload="clickButton()">

    <form>
    <input type="button" id="button1" onclick="alertMsg()" value="Button 1" />
    </form>

    </body>
    </html>
鲍志强 wechat
欢迎你扫一扫上面的微信公众号,订阅我的博客!
你的点赞是为了你的未来