js设计模式系列之(二)订阅发布模式

订阅发布模式如果按数学翻译其实就是,一对多的映射关系。怎么解释呢? 就是一个开关,同时并联几个灯泡(在不同房间),触发的时候,几个灯泡都会得到指令,然后执行发光的行为。首先,这几个灯泡,并不知道我和哪些灯泡连在一起(其实,也不用知道),只需要你给一个按下开关的指令就over了。
懂么? 不懂~
行,大爷,我这还有一个栗子==>《收音机和电台的午夜故事》
午夜时分,电台正发送着无线电波(触发事件), 这时候小明,打开了收音机,拨到固定的频率(订阅事件), 突然,电台里响起了18岁少女般的声音(触发事件). 此时此刻,还有无数和小明一样,使用着收音机收听的人,但是他们之间相互都不认识。 只是他们都在听一个电台.

订阅发布模式

这种模式在js里面有着天然的优势,因为js本身就是事件驱动型语言。比如,页面上有一个button, 你点击一下就会触发上面的click事件,而此时有一部分程序正在监听这个事件,随之触发相关的处理程序.

1
2
3
4
var button = $("#button");
button.on("click",function(){
console.log("I am pressing the button");
});

事实上,我们也早就熟悉这个模式了,只是不知道这叫什么(订阅发布模式 又名 观察者模式).
这个模式最大的一个好处就在于,能够解耦回调函数,让你的程序看起来更美观(虽然现在有Promise和Deferred帮忙,但是不彻底)。

订阅发布模式内涵

说了点理论,来些干货.
订阅发布模式无非就两个部分,一个订阅(监听程序),一个发布(触发事件).而他们中间的链接枢纽就是事件。 通常来说,我们可以自定义一个订阅发布模式–使用自定义事件.
(由于IE过于SB,我就不想说他的事了,下面这些适用于chrome, iE9+,ff等其他现代浏览器中)

1
2
3
4
5
6
7
8
9
10
11
12
13
 // Create the event.  //创建一个事件
var event = document.createEvent('Event'); //Event是自定义的事件名

// Define that the event name is 'build'.
event.initEvent('build', true, true); //这里是初始化事件, //就是些参数而已.

// Listen for the event.
elem.addEventListener('build', function (e) { //给事件添加监听
// e.target matches elem
}, false);

// target can be any Element or other EventTarget.
elem.dispatchEvent(event); //触发事件

大致就是这几个步骤,由于这样写,太非人道了。所以这里映入jquery的trigger触发方式(大哥就是大哥~)
在jquery的事件处理中,几个基本和事件相关的API需要熟悉。一个是on,一个是trigger.

1
2
3
4
$(".ele").on("click",function(){
console.log("clicking");
});
$(".ele").trigger("click");

这是一个基本的使用,使用trigger来触发事件.
但是谁尼玛无聊到连click自己手动触发啊,这个例子只是讲解。现在说一下精华-自定义事件.
在jquery里面,可以直接使用on来进行自定义事件的模拟。

1
2
3
4
ele.on("stimulate",function(){  //订阅一个事件
...do sth
});
ele.trigger("stimulate"); //发布一个事件

这里trigger只是起到一个开关的作用,那么我想要他变为一个管道可以吗?
absolutely!!!
在trigger里面还有第二个参数可以选择,即[data]

1
2
3
4
$(document.body).on("stimulate",function(event,name1,name2){  //和节点有关的事件里,第一个参数永远是event
console.log(name1,name2); //"jimmy","sam"
});
$(document.body).trigger("stimulate",["jimmy","sam"]);

而且如果你自定义事件过多,起名也是件死人的事。所以牛逼的jq会帮你把命名空间处理好.

1
2
3
ele.on("stimulate.name.age",function(){  //使用"."链接
...do sth
});

他的作用域就是 < stimulate >包含< name >包含< age>这样一个关系.
详情可以参考: Aron大神些的jquery事件解析。这里我直接把trigger的源码贴出来吧.以供参考.
trigger源码
其实上面的自定义事件的用法也非常有限,因为如果使用一个节点作为载体的话,这样的成本也太大了。所以一般在业内已经有成熟的自定义事件的插件了.
在nodeJS里面,我们可以使用自带的一个模块–EventEmitter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var Emitter = require('events').EventEmitter;

var emitter = new Emitter();

emitter.on('oneEvent',function(stream){

console.log(`the first subscribe is ${stream}`);

});

emitter.on('oneEvent',function(stream){

console.log(`the second subscribe is ${stream}`);

});

emitter.emit('oneEvent','I am a stream!');

ok,童鞋,如果有兴趣,可以搬到nodeJS环境下运行,看看结果是神马。当然,在前端jquery的插件sub/pub 也是一个订阅发布模式很好的实践。

订阅发布模式进阶

市面上流行很多订阅发布模式的插件,这里我就不一一举例了,但是,订阅发布模式的Core 就只有几个,我们自己也能简单的模拟一个。 订阅发布模式,不就是订阅发布事件么~
好,我们就这3个部分来自己模拟一个。

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
var imitate = (function() {
var imitate = {
clientList: [],
listen: function(key, fn) {
if (!this.clientList[key]) {
this.clientList[key] = [];
}
this.clientList[key].push(fn);
},
trigger: function() {
var key = [].shift.call(arguments);
var fns = this.clientList[key];

// 如果没有对应的绑定消息
if (!fns || fns.length === 0) {
return false;
}

for (var i = 0, fn; fn = fns[i++];) {
// arguments 是 trigger带上的参数
fn.apply(this, arguments);
}
}
}
return function() {
return Object.create(imitate);
}
})();
var eventModel = imitate(); //得到上面的对象
eventModel.listen("jimmy",function(){console.log("jimmy");}); //jimmy
eventModel.trigger("jimmy");

恩,这样就可以简单的模拟一个订阅发布模式的简单模块。
当然,这样写改进的空间还是挺大的。解决命名空间问题(暂不管),删除订阅问题(这个用处不大)…目前我们先着手解决这个问题.

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
var Event = (function() {
var clientList = {};
var listen,
trigger,
remove;
listen = function(key, fn) {
if (!clientList[key]) {
clientList[key] = [];
}
clientList[key].push(fn);
};

trigger = function() {
var key = [].shift.call(arguments);
var 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];

// key对应的消息没有被人订阅
if (!fns) {
return false;
}

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

return {
listen: listen,
trigger: trigger,
remove: remove
}
}());

这个Event对象,能够解决大部分事件模拟的问题。说了这么多,md,实例嘞。。。等等。马上来

发布订阅模式的实战

如果大家写过登录框(异步登录哈),应该知道.登录框和header的部分是完全不同的两个部分。这个场景就很适合发布订阅模式了。
看一下。如果没有发布订阅模式的代码:

1
2
3
4
5
6
7
8
9
10
login.on("click",function(){
var name = $(".username").val().trim;
http.login(name) //使用异步Deferred书写
.then(function(data){ //以下填写乱七八糟的处理
changeName();
changeAvtar();
changeStatus();
...
})
});

使用发布订阅模式

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
login.on("click",function(){
var name = $(".username").val().trim;
http.login(name) //使用异步Deferred书写
.then(function(data){
Event.trigger("login",data); //发布我登录成功的状态,并传入参数
})
});
var header = (function() {
Event.listen("login", function(data) {
header.changeAvator(data);
})
return {
changeAvator: function(data) {
...换头像
}
}
})();
var bar = (function() {
Event.listen("login", function(data) {
bar.changeName(data);
})
return {
changeName: function(data) {
...换名字
}
}
})();

可以清楚的看到,如果你的登录状态改变了的话,会有一系列的订阅程序发生.而且每个订阅之间互不干扰,你可以随便添加或者删除订阅,这都不会影响你的登录的执行逻辑.

模块间通信

另外,该模式也是实现模块间通信一个很重要的手段。 所谓的通信,其实就是传递参数,我们可以很好的利用emitter时,传递的data参数,来进行信息的传递。 比如,我有一个记录你点击次数的业务, 点击计数模块我放一边,显示计数模块我放一边。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//计数模块
var calculate = (function(){
var count = 0;
var button = document.getElementById( 'count' );
button.onclick = function(){
Event.trigger( 'add', count++ );
}
})();
//显示模块
var subscriber = (function(){
var div = document.getElementById( 'show' );
Event.listen( 'add', function( count ){ //通过count进行消息的传递
div.innerHTML = count;
});
})();

我们先意淫一下,上面写的就是两个模块, 通过trigger来发出传递效果,通过data来获得传递信息。
但是使用的时候,一定要慎重,订阅的越多,bug的查找也会越复杂。因为你一旦更改了data的格式,涉及到的订阅事件也需要更改,如果订阅数过多,那么你data改动的成本也就越大,出现bug时,data的修复能力也就越弱.(Ps: 当然,你使用json进行内容的分区也是可以的。但是复杂度还是这么多~ (づ ̄ 3 ̄)づ).

本人目前正在找前端,希望有哪位大大好心,能帮我内推一下 - 邮箱是 villainthr@gmail.com

猫友会其事

本无心插柳, 却慢慢有一定影响力,猫友会是一个邀请制的互联网优秀人才的交流平台, 以鄂籍或在鄂求学过的同学们为主。 目前在推进的一些事情:

  • 猫友会大讲坛,分享产品/技术/创业心得
  • 猫友信息平台,对接武汉最优秀的团队以及愿意回武汉的优秀人才。
  • 猫友公众号,优质原创产品/技术文章
  • 猫友读书会,阅读+思考+总结,筹备中

长按关注猫头鹰技术公众号(mtydev)

img

留言

本站总访问量

留言