设计模式之发布/订阅模式(Publish/Subscribe)
文章目录
发布/订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在 JavaScript 开发中,我们一般用事件模型来替代传统的发布/订阅模式
遍地的发布订阅现象
如今的信息化时代,发布/订阅模式的应用可以说非常广泛,比如微信公众号就是典型的发布/订阅模式,公众号发布一条信息,所有的订阅者都会收到。
有人可能也会想到经常收到的各种广告短信信息(有的可能是被动订阅),其实发送短信通知或广告也是一个典型的发布/订阅模式。
发布/订阅模式可以广泛用于异步编程中,代替传递回掉函数的方案,比如,我们可以订阅 ajax 请求的 error、succ 等事件。
另外发表订阅让两个对象松耦合在一起,不必了解彼此细节,当有新的订阅者出现时,发布者的代码不需要任何修改。同样发布者需要改变时,也不会影响到之前的订阅者。只要之前约定的事件名没有变化,就可以自由地改变它们。
定义
发布订阅模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。
使用发布订阅模式的好处:
- 支持简单的广播通信,自动通知所有已经订阅过的对象。
- 页面载入后目标对象很容易与观察者存在一种动态关联,增加了灵活性。
- 目标对象与观察者之间的抽象耦合关系能够单独扩展以及重用。
使用实例
自定义发表订阅
我们来尝试一个自定义的发布订阅模式,那么如何实现发布订阅呢
- 指定一个发布者
- 给发布者添加一个缓冲列表,用于存放回调函数以用于通知订阅者
- 发布消息时,发布者遍历缓存列表,依次触发每个订阅者的回调函数
一个简单的天气状态订阅
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var Weather = { list: [], // 缓存列表 listen: function(fn) { // 增加订阅者 this.list.push(fn) }, publish: function() { // 发布消息 for(var i=0,fn; fn=this.list[i++];) { fn.apply(this,arguments); } } }; // 订阅消息 Weather.listen(function(weather, wind){ console.log('天气:' + weather, '风力:'+ wind); }) // 发布消息 Weather.publish("晴天","微风"); // 天气:晴天 风力:微风 Weather.publish("雷阵雨","5级风"); // 天气:雷阵雨 风力:5级风 |
以上,已经实现了一个最简单的发布—订阅模式,还可以为订阅者增加自选功能,订阅自己想要的消息,也可以增加取消订阅的事件。
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 |
var PubSub = { list: [], listen: function(key, fn){ if(!this.list[key]) { this.list[key]=[]; } this.list[key].push(fn); }, publish: function(){ var key = Array.prototype.shift.call(arguments), fns = this.list[key]; if(!fns || fns.length === 0) { return false; } for(var i = 0, fn; fn = fns[i++];){ fn.apply(this, arguments); } } } // var installEvent = function(obj) { for (var i in PubSub) { obj[i] = PubSub[i]; } }; var day = {} installEvent(day); day.listen('天气', function(wind) { console.log('风力:'+ wind); }); day.publish('天气', "8级风"); |
实战之网站登录
网站登录是最常见的形式,通常在登录以后我们会ajax异步请求获取用户信息,比如显示用户名字、头像等信息在header模块,而这两个字段都是来自用户登录后返回的信息。至于 ajax 请求什么时候能成功返回用户信息,这点我们没有办法确定,虽然现在看起来和发布订阅模式没关系,因为异步的问题通常也可以回调函数来解决。
我们不知道除了 header 头部、nav 导航、消息列表、购物车之外,将来还有哪些模块需要使用这些用户信息。如果它们和用户信息模块产生了强耦合,比如下面这样的形式:
1 2 3 4 5 6 |
login.succ(function(data){ header.setAvatar( data.avatar); // 设置 header 模块的头像 nav.setAvatar( data.avatar ); // 设置导航模块的头像 message.refresh(); // 刷新消息列表 cart.refresh(); // 刷新购物车列表 }); |
现在登录模块是我们负责编写的,但我们还必须了解 header 模块里设置头像的方法叫 setAvatar、购物车模块里刷新的方法叫 refresh,这种耦合性会使程序变得僵硬,header 模块不能随意再改变 setAvatar 的方法名,它自身的名字也不能被改为 header1、header2。 这是针对具 体实现编程的典型例子,针对具体实现编程是不被赞同的。
某一个,项目新增加收获地址管理模块:
1 2 3 4 5 6 |
login.succ(function(data){ header.setAvatar( data.avatar); nav.setAvatar( data.avatar ); message.refresh(); address.refresh(); // 新增加收获地址 }); |
现在我们用发布订阅重写,对用户信息感兴趣的业务模块将自行订阅登录成功的消息事件。 当登录成功时,登录模块只需要发布登录成功的消息,而业务方接受到消息之后,就会开始进行各自的业务处理,登录模块并不关心业务方究竟要做什么,也不想去了解它们的内部细节。改善后的代码如下:
1 2 3 |
$.ajax( 'http://xxx.com?login', function(data){ // 登录成功 login.trigger( 'loginSucc', data); // 发布登录成功的消息 }); |
各模块监听登录成功的消息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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 模块的头像'); } } })(); |
如果有一天在登录完成之 后,又增加一个刷新收货地址列表的行为,那么只要在收货地址模块里加上监听消息的方法即可,而这可以让开发该模块的同事自己完成,你作为登录模块的开发者,永远不用再关心这些行为了
1 2 3 4 5 6 7 8 9 10 |
var address = (function(){ // 收获地址模块 login.listen('loginSucc', function(obj){ address.refresh(obj); }); return { refresh: function( avatar ){ console.log( '刷新收货地址列表' ); } } })(); |
总结
发布订阅的使用场合就是:当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。
总的来说,发布订阅模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化。
另外, 发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一起的时候,要跟踪一个 bug 不是件轻松的事情。
参考引用资料
《JavaScript设计模式与开发实践》
文章作者 ZHIKING
上次更新 2017-04-05