关键词:
概述
观察者模式又叫发布 – 订阅模式(Publish/Subscribe),它定义了一种一对多的关系,让多个观察者对象同时监听某一个目标对象(为了方便理解,以下将观察者对象叫做订阅者,将目标对象叫做发布者)。发布者的状态发生变化时就会通知所有的订阅者,使得它们能够自动更新自己。
观察者模式的使用场合就是:当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。
观察者模式的中心思想就是促进松散耦合,一为时间上的解耦,二为对象之间的解耦。让耦合的双方都依赖于抽象,而不是依赖于具体,从而使得各自的变化都不会影响到另一边的变化。
实现
(function (window, undefined) { var _subscribe = null, _publish = null, _unsubscribe = null, _shift = Array.prototype.shift, // 删除数组的第一个 元素,并返回这个元素 _unshift = Array.prototype.unshift, // 在数组的开头添加一个或者多个元素,并返回数组新的length值 namespaceCache = {}, _create = null, each = function (ary, fn) { var ret = null; for (var i = 0, len = ary.length; i < len; i++) { var n = ary[i]; ret = fn.call(n, i, n); } return ret; }; // 订阅消息 _subscribe = function (key, fn, cache) { if (!cache[key]) { cache[key] = []; } cache[key].push(fn); }; // 取消订阅(取消全部或者指定消息) _unsubscribe = function (key, cache, fn) { if (cache[key]) { if (fn) { for (var i = cache[key].length; i >= 0; i--) { if (cache[key][i] === fn) { cache[key].splice(i, 1); } } } else { cache[key] = []; } } }; // 发布消息 _publish = function () { var cache = _shift.call(arguments), key = _shift.call(arguments), args = arguments, _self = this, ret = null, stack = cache[key]; if (!stack || !stack.length) { return; } return each(stack, function () { return this.apply(_self, args); }); }; // 创建命名空间 _create = function (namespace) { var namespace = namespace || "default"; var cache = {}, offlineStack = {}, // 离线事件,用于先发布后订阅,只执行一次 ret = { subscribe: function (key, fn, last) { _subscribe(key, fn, cache); if (!offlineStack[key]) { offlineStack[key] = null; return; } if (last === "last") { // 指定执行离线队列的最后一个函数,执行完成之后删除 offlineStack[key].length && offlineStack[key].pop()(); // [].pop => 删除一个数组中的最后的一个元素,并且返回这个元素 } else { each(offlineStack[key], function () { this(); }); } offlineStack[key] = null; }, one: function (key, fn, last) { _unsubscribe(key, cache); this.subscribe(key, fn, last); }, unsubscribe: function (key, fn) { _unsubscribe(key, cache, fn); }, publish: function () { var fn = null, args = null, key = _shift.call(arguments), _self = this; _unshift.call(arguments, cache, key); args = arguments; fn = function () { return _publish.apply(_self, args); }; if (offlineStack && offlineStack[key] === undefined) { offlineStack[key] = []; return offlineStack[key].push(fn); } return fn(); } }; return namespace ? (namespaceCache[namespace] ? namespaceCache[namespace] : namespaceCache[namespace] = ret) : ret; }; window.pubsub = { create: _create, // 创建命名空间 one: function (key, fn, last) { // 订阅消息,只能单一对象订阅 var pubsub = this.create(); pubsub.one(key, fn, last); }, subscribe: function (key, fn, last) { // 订阅消息,可多对象同时订阅 var pubsub = this.create(); pubsub.subscribe(key, fn, last); }, unsubscribe: function (key, fn) { // 取消订阅,(取消全部或指定消息) var pubsub = this.create(); pubsub.unsubscribe(key, fn); }, publish: function () { // 发布消息 var pubsub = this.create(); pubsub.publish.apply(this, arguments); } }; })(window, undefined);
应用
假如我们正在开发一个商城网站,网站里有header头部、nav导航、消息列表、购物车等模块。这几个模块的渲染有一个共同的前提条件,就是必须先用ajax异步请求获取用户的登录信息。
至于ajax请求什么时候能成功返回用户信息,这点我们没有办法确定。更重要的一点是,我们不知道除了header头部、nav导航、消息列表、购物车之外,将来还有哪些模块需要使用这些用户信息。如果它们和用户信息模块产生了强耦合,比如下面这样的形式:
login.succ(function (data) { header.setAvatar(data.avatar); // 设置header模块的头像 nav.setAvatar(data.avatar); // 设置导航模块的头像 message.refresh(); // 刷新消息列表 cart.refresh(); // 刷新购物车列表 });
现在登录模块是由你负责编写的,但我们还必须了解header模块里设置头像的方法叫setAvatar、购物车模块里刷新的方法叫refresh,这种耦合性会使程序变得僵硬,header模块不能随意再改变setAvatar的方法名。这是针对具体实现编程的典型例子,针对具体实现编程是不被赞同的。
等到有一天,项目中又新增了一个收货地址管理的模块,这个模块是由另一个同事所写的,此时他就必须找到你,让你登录之后刷新一下收货地址列表。于是你又翻开你3个月前写的登录模块,在最后部分加上这行代码:
login.succ(function (data) { header.setAvatar(data.avatar); nav.setAvatar(data.avatar); message.refresh(); cart.refresh(); address.refresh(); // 增加这行代码 });
我们就会越来越疲于应付这些突如其来的业务要求,不停地重构这些代码。
用观察者模式重写之后,对用户信息感兴趣的业务模块将自行订阅登录成功的消息事件。当登录成功时,登录模块只需要发布登录成功的消息,而业务方接受到消息之后,就会开始进行各自的业务处理,登录模块并不关心业务方究竟要做什么,也不想去了解它们的内部细节。改善后的代码如下:
$.ajax(‘http:// xxx.com?login‘, function(data) { // 登录成功 pubsub.publish(‘loginSucc‘, data); // 发布登录成功的消息 }); // 各模块监听登录成功的消息: var header = (function () { // header模块 pubsub.subscribe(‘loginSucc‘, function(data) { header.setAvatar(data.avatar); }); return { setAvatar: function(data){ console.log(‘设置header模块的头像‘); } }; })(); var nav = (function () { // nav模块 pubsub.subscribe(‘loginSucc‘, function(data) { nav.setAvatar(data.avatar); }); return { setAvatar: function(avatar) { console.log(‘设置nav模块的头像‘); } }; })();
如上所述,我们随时可以把setAvatar的方法名改成setTouxiang。如果有一天在登录完成之后,又增加一个刷新收货地址列表的行为,那么只要在收货地址模块里加上监听消息的方法即可,而这可以让开发该模块的同事自己完成,你作为登录模块的开发者,永远不用再关心这些行为了。代码如下:
var address = (function () { // 地址模块 pubsub.subscribe(‘loginSucc‘, function(obj) { address.refresh(obj); }); return { refresh: function(avatar) { console.log(‘刷新收货地址列表‘); } }; })();
优缺点
优点
- 支持简单的广播通信,自动通知所有已经订阅过的对象;
- 页面载入后发布者很容易与订阅者存在一种动态关联,增加了灵活性;
- 发布者与订阅者之间的抽象耦合关系能够单独扩展以及重用。
缺点
- 创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中;
- 虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。
参考
- 《JavaScript设计模式与开发实践》 第 8 章 发布—订阅模式
- 《JavaScript设计模式》 第 9 章 第 5 节 Observer(观察者)模式
- http://www.cnblogs.com/TomXu/archive/2012/03/02/2355128.html
摘javascript设计模式与开发实践--单例模式
本文章所有内容均摘自《Javascript设计模式与开发实践》一书(有兴趣的可以购买),加入了一点点自己的理解,写这篇文章的目的是,加强自身对设计模式的理解,以及对于没有接触过这一块的入门者的参考。阅读本章内容,... 查看详情
《javascript设计模式与开发实践》——策略模式
...一个个封装起来。将不变的部分和变化的部分隔开是每个设计模式的主题,策略模式也不例外,策略模式的目的就是将算法的使用与算法的实现分离开来。一个基于策略模式的程序至少由两部分组成。第一个部 查看详情
《javascript设计模式与开发实践》学习之使用策略模式计算奖金
简洁的做法如下: 查看详情
javascript设计模式与开发实践
...this、call和apply 阅读第3章 闭包和高阶函数 第二部分 设计模式 第4章 单例模式 第5章 策略模式 第6章 代理模式 第7章 迭代器模式 第8章 发布—订阅模式 第9章 命令模式 第10章 组合模式 第11章 模板方法模式 第12... 查看详情
javascript设计模式与开发实践单例模式
单例模式定义:保证一个类仅有有一个实例,并提供一个访问它的全局访问点。应用场景:有一些对象只需要一个。如线程池、全局缓存、浏览器中的window对象等。 实现一:varSingleton=function(name){this.name=name;this.instance=nul... 查看详情
javascript设计模式与开发实践享元模式
享元(flyweight)模式是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。 如果系统中因为创建了大量类似的对象而导致内存占用过高,... 查看详情
《javascript设计模式与开发实践》——单例模式
单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的window对象等。在JavaScript开发中,单例模式的... 查看详情
javascript设计模式与开发实践装饰者模式
在程序开发中,许多时候都并不希望某个类天生就非常庞大,一次性包含许多职责。那么我们就可以使用装饰者模式。装饰者模式可以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。 装... 查看详情
《javascript设计模式与开发实践》——代理模式
代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。... 查看详情
javascript设计模式与开发实践模板方法模式
一、模板方法模式的定义和组成 模板方法模式是一种只需使用继承就可以实现的非常简单的模式。 模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算... 查看详情
javascript设计模式与开发实践阅读笔记(10)——组合模式
组合模式:一些子对象组成一个父对象,子对象本身也可能是由一些孙对象组成。 有点类似树形结构的意思,这里举一个包含命令模式的例子1varlist=function(){//创建接口对象的函数2return{3arr:[],//执行列表用来存储需要执行的... 查看详情
javascript设计模式与开发实践迭代器模式
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按... 查看详情
javascript设计模式与开发实践阅读笔记——迭代器模式
迭代器模式:指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序... 查看详情
javascript设计模式与开发实践适配器模式
适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。 适配器的别名是包装器(wrapper),这是一个相对简单的模式。在程... 查看详情
读书javascript设计模式与开发实践
2016.08.30 《JavaScript设计模式与开发实践》曾探人民邮电出版社2016年5月第1版p13找到变化的部分并封装之,以使得容易替换;而剩下的就是不变的部分。 P49函数柯里化(currying)的作用是多次收集参数,然后作为数组传给处理... 查看详情
javascript设计模式与开发实践阅读笔记——代理模式
代理模式:是为一个对象提供一个代用品或占位符,以便控制对它的访问。代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象... 查看详情
javascript设计模式与开发实践阅读笔记——命令模式
命令模式:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。 说法很... 查看详情
javascript设计模式与开发实践阅读笔记——策略模式
策略模式:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。我的理解就是把各种方法封装成函数,同时存在一个可以调用这些方法的公共函数。这样做的好处是可以消化掉内部的分支判断,使代码效率更... 查看详情