谈谈工作中的设计模式

前言

记得刚毕业的时候参加了一次校招面试,之前表现的很好,最后时面试官问我懂不懂设计模式,我说不懂,然后就进去了;后面又参加了某大公司的校招,开始表现还行,后面面试官问我懂不懂设计模式,我说懂(上次后补习了下),最后把工厂模式的代码背写到了纸上,然后就没有然后了......

现在回想起来当时有点傻有点天真,没有几十万的代码量,没有一定的经验总结,居然敢说懂设计模式,这不是找抽么?

经过这几年工作学习,感觉是时候系统的回忆下平时工作内容,看用到了什么设计模式,权当总结。

小钗对设计模式的理解程度有限,文中不足之处请您拍砖。

面向对象的实现

设计模式便是面向对象的深入,面向对象的应用,所以类的实现是第一步:

PS:这里依赖了underscore,各位自己加上吧。

 1 //window._ = _ || {};
 2 // 全局可能用到的变量
 3 var arr = [];
 4 var slice = arr.slice;
 5 /**
 6 * inherit方法,js的继承,默认为两个参数
 7 *
 8 * @param  {function} origin  可选,要继承的类
 9 * @param  {object}   methods 被创建类的成员,扩展的方法和属性
10 * @return {function}         继承之后的子类
11 */
12 _.inherit = function (origin, methods) {
13 
14   // 参数检测,该继承方法,只支持一个参数创建类,或者两个参数继承类
15   if (arguments.length === 0 || arguments.length > 2) throw '参数错误';
16 
17   var parent = null;
18 
19   // 将参数转换为数组
20   var properties = slice.call(arguments);
21 
22   // 如果第一个参数为类(function),那么就将之取出
23   if (typeof properties[0] === 'function')
24     parent = properties.shift();
25   properties = properties[0];
26 
27   // 创建新类用于返回
28   function klass() {
29     if (_.isFunction(this.initialize))
30       this.initialize.apply(this, arguments);
31   }
32 
33   klass.superclass = parent;
34 
35   // 父类的方法不做保留,直接赋给子类
36   // parent.subclasses = [];
37 
38   if (parent) {
39     // 中间过渡类,防止parent的构造函数被执行
40     var subclass = function () { };
41     subclass.prototype = parent.prototype;
42     klass.prototype = new subclass();
43 
44     // 父类的方法不做保留,直接赋给子类
45     // parent.subclasses.push(klass);
46   }
47 
48   var ancestor = klass.superclass && klass.superclass.prototype;
49   for (var k in properties) {
50     var value = properties[k];
51 
52     //满足条件就重写
53     if (ancestor && typeof value == 'function') {
54       var argslist = /^\s*function\s*\(([^\(\)]*?)\)\s*?\{/i.exec(value.toString())[1].replace(/\s/i, '').split(',');
55       //只有在第一个参数为$super情况下才需要处理(是否具有重复方法需要用户自己决定)
56       if (argslist[0] === '$super' && ancestor[k]) {
57         value = (function (methodName, fn) {
58           return function () {
59             var scope = this;
60             var args = [
61               function () {
62                 return ancestor[methodName].apply(scope, arguments);
63               }
64             ];
65             return fn.apply(this, args.concat(slice.call(arguments)));
66           };
67         })(k, value);
68       }
69     }
70 
71     //此处对对象进行扩展,当前原型链已经存在该对象,便进行扩展
72     if (_.isObject(klass.prototype[k]) && _.isObject(value) && (typeof klass.prototype[k] != 'function' && typeof value != 'fuction')) {
73       //原型链是共享的,这里处理逻辑要改
74       var temp = {};
75       _.extend(temp, klass.prototype[k]);
76       _.extend(temp, value);
77       klass.prototype[k] = temp;
78     } else {
79       klass.prototype[k] = value;
80     }
81 
82   }
83 
84   if (!klass.prototype.initialize)
85     klass.prototype.initialize = function () { };
86 
87   klass.prototype.constructor = klass;
88 
89   return klass;
90 };

使用测试:

 1 var Person = _.inherit({
 2     initialize: function(opts) {
 3         this.setOpts(opts);
 4     },
 5 
 6     setOpts: function (opts) {
 7         for(var k in opts) {
 8             this[k] = opts[k];
 9         }
10     },
11 
12     getName: function() {
13         return this.name;
14     },
15 
16     setName: function (name) {
17         this.name = name
18     }
19 });
20 
21 var Man = _.inherit(Person, {
22     initialize: function($super, opts) {
23         $super(opts);
24         this.sex = 'man';
25     },
26 
27     getSex: function () {
28         return this.sex;
29     }
30 });
31 
32 var Woman = _.inherit(Person, {
33     initialize: function($super, opts) {
34         $super(opts);
35         this.sex = 'women';
36     },
37 
38     getSex: function () {
39         return this.sex;
40     }
41 });
42 
43 var xiaoming = new Man({
44     name: '小明'
45 });
46 
47 var xiaohong = new Woman({
48     name: '小红'
49 });
xiaoming.getName()
"小明"
xiaohong.getName()
"小红"
xiaoming.getSex()
"man"
xiaohong.getSex()
"women"

单例模式(Singleton)

单列为了保证一个类只有一个实例,如果不存在便直接返回,如果存在便返回上一次的实例,其目的一般是为了资源优化。

javascript中实现单例的方式比较多,比较实用的是直接使用对象字面量:

1 var singleton = {
2     property1: "property1",
3     property2: "property2",
4     method1: function () {}
5 };

类实现是正统的实现,一般是放到类上,做静态方法:

在实际项目中,一般这个应用会在一些通用UI上,比如mask,alert,toast,loading这类组件,还有可能是一些请求数据的model,简单代码如下:

 1 //唯一标识,一般在amd模块中
 2 var instance = null;
 3 
 4 //js不存在多线程,这里是安全的
 5 var UIAlert = _.inherit({
 6     initialize: function(msg) {
 7         this.msg = msg;
 8     },
 9     setMsg: function (msg) {
10         this.msg = msg;
11     },
12     showMessage: function() {
13         console.log(this.msg);
14     }
15 });
16 
17 var m1 = new UIAlert('1');
18 m1.showMessage();//1
19 var m2 = new UIAlert('2');
20 m2.showMessage();//2
21 m1.showMessage();//1

 如所示,这个是一个简单的应用,如果稍作更改的话:

 1 //唯一标识,一般在amd模块中
 2 var instance = null;
 3 
 4 //js不存在多线程,这里是安全的
 5 var UIAlert = _.inherit({
 6     initialize: function(msg) {
 7         this.msg = msg;
 8     },
 9     setMsg: function (msg) {
10         this.msg = msg;
11     },
12     showMessage: function() {
13         console.log(this.msg);
14     }
15 });
16 UIAlert.getInstance = function () {
17     if (instance instanceof this) {
18         return instance;
19     } else {
20         return instance = new UIAlert(); //new this
21     }
22 }
23 
24 var m1 = UIAlert.getInstance();
25 m1.setMsg(1);
26 m1.showMessage();//1
27 var m2 = UIAlert.getInstance();
28 m2.setMsg(2);
29 m2.showMessage();//2
30 m1.showMessage();//2

如所示,第二次的改变,影响了m1的值,因为他们的实例是共享的,这个便是一次单例的使用,而实际场景复杂得多。

以alert组件为例,他还会存在按钮,一个、两个或者三个,每个按钮事件回调不一样,一次设置后,第二次使用时各个事件也需要被重置,比如事件装在一个数组eventArr = []中,每次这个数组需要被清空重置,整个组件的dom结构也会重置,好像这个单例的意义也减小了,真实的意义在于全站,特别是对于webapp的网站,只有一个UI dom的根节点,这个才是该场景的意义所在。

而对mask而言便不太适合全部做单例,以弹出层UI来说,一般都会带有一个mask组件,如果一个组件弹出后马上再弹出一个,第二个mask如果与第一个共享的话便不合适了,因为这个mask应该是各组件独享的。

单例在javascript中的应用更多的还是来划分命名空间,比如underscore库,比如以下场景:

① Hybrid桥接的代码

window.Hybrid = {};//存放所有Hybrid的参数

② 日期函数

window.DateUtil = {};//存放一些日期操作方法,比如将“2015年2月14日”这类字符串转换为日期对象,或者逆向转换

......

工厂模式(Factory)

工厂模式是一个比较常用的模式,介于javascript对象的不定性,其在前端的应用门槛更低。

工厂模式出现之初意在解决对象耦合问题,通过工厂方法,而不是new关键字实例化具体类,将所有可能的类的实例化集中在一起。

一个最常用的例子便是我们的Ajax模块:

 1 var XMLHttpFactory = {};
 2 var XMLHttpFactory.createXMLHttp = function() {
 3     var XMLHttp = null;
 4     if (window.XMLHttpRequest){
 5     XMLHttp = new XMLHttpRequest()
 6     }else if (window.ActiveXObject){
 7     XMLHttp = new ActiveXObject("Microsoft.XMLHTTP")
 8     }
 9     return XMLHttp;
10 }

使用工厂方法的前提是,产品类的接口需要一致,至少公用接口是一致的,比如我们这里有一个需求是这样的:

可以看到各个模块都是不一样的:

① 数据请求

② dom渲染,样式也有所不同

③ 事件交互

但是他们有一样是相同的:会有一个共同的事件点:

① create

② show

③ hide

所以我们的代码可以是这样的:

 1 var AbstractView = _.inherit({
 2     initialize: function() {
 3         this.wrapper = $('body');
 4         //事件管道,实例化时触发onCreate,show时候触发onShow......
 5         this.eventsArr = [];
 6     },
 7     show: function(){},
 8     hide: function (){}
 9 });
10 var SinaView = _.inherit(AbstractView, {
11 });
12 var BaiduView = _.inherit(AbstractView, {
13 });

每一个组件实例化只需要执行实例化操作与show操作即可,各个view的显示逻辑在自己的事件管道实现,真实的逻辑可能是这样的

 1 var ViewContainer = {
 2     SinaView: SinaView,
 3     BaiduView: BaiduView
 4 };
 5 var createView = function (view, wrapper) {
 6     //这里会有一些监测工作,事实上所有的view类应该放到一个单列ViewContainer中
 7     var ins = new ViewContainer[view + 'View'];
 8     ins.wrapper = wrapper;
 9     ins.show();
10 }
11 //数据库读出数据
12 var moduleInfo = ['Baidu', 'Sina', '...'];
13 
14 for(var i = 0, len = moduleInfo.length; i < len; i++){
15     createView(moduleInfo[i]);
16 }

如之前写的坦克大战,创建各自坦克工厂模式也是绝佳的选择,工厂模式暂时到此。

桥接模式(bridge)

桥接模式一个非常典型的使用便是在Hybrid场景中,native同事会给出一个用于桥接native与H5的模块,一般为bridge.js。

native与H5本来就是互相独立又互相变化的,如何在多个维度的变化中又不引入额外复杂度,这个时候bridge模式便派上了用场,使抽象部分与实现部分分离,各自便能独立变化。

这里另举一个应用场景,便是UI与其动画类,UI一般会有show的动作,通常便直接显示了出来,但是我们实际工作中需要的UI显示是:由下向上动画显示,由上向下动画显示等效果。

这个时候我们应该怎么处理呢,简单设计一下:

 1 var AbstractView = _.inherit({
 2     initialize: function () {
 3         //这里的dom其实应该由template于data组成,这里简化
 4         this.$el = $('<div style="display: none; position: absolute; left: 100px; top: 100px; border: 1px solid #000000;">组件</div>');
 5         this.$wrapper = $('body');
 6         this.animatIns = null;
 7     },
 8     show: function () {
 9         this.$wrapper.append(this.$el);
10         if(!this.animatIns) {
11             this.$el.show();
12         } else {
13             this.animatIns.animate(this.$el, function(){});
14         }
15         //this.bindEvents();
16     }
17 });
18 
19 var AbstractAnimate = _.inherit({
20     initialize: function () {
21     },
22     //override
23     animate: function (el, callback) {
24         el.show();
25         callback();
26     }
27 });
28 
29 
30 var UPToDwonAnimate = _.inherit(AbstractAnimate, {
31     animate: function (el, callback) {
32         //动画具体实现不予关注,这里使用zepto实现
33         el.animate({
34             'transform': 'translate(0, -250%)'
35         }).show().animate({
36             'transform': 'translate(0, 0)'
37         }, 200, 'ease-in-out', callback);
38     }
39 });
40 
41 
42 var UIAlert = _.inherit(AbstractView, {
43     initialize: function ($super, animateIns) {
44         $super();
45         this.$el = $('<div style="display: none; position: absolute; left: 100px; top: 200px; border: 1px solid #000000;">alert组件</div>');
46         this.animatIns = animateIns;
47     }
48 });
49 
50 var UIToast = _.inherit(AbstractView, {
51     initialize: function ($super, animateIns) {
52         $super();
53         this.animatIns = animateIns;
54     }
55 });
56 
57 var t = new UIToast(new UPToDwonAnimate);
58 t.show();
59 
60 var a = new UIAlert();
61 a.show();

这里组件对动画类库有依赖,但是各自又不互相影响(事实上还是有一定影响的,比如其中一些事件便需要动画参数触发),这个便是一个典型的桥接模式。

再换个方向理解,UI的css样式事实上也可以做到两套系统,一套dom结构一套皮肤库,但是这个实现上有点复杂,因为html不可分割,而动画功能这样处理却比较合适。

装饰者模式(decorator)

装饰者模式的意图是为一个对象动态的增加一些额外职责;是类继承的另外一种选择,一个是编译时候增加行为,一个是运行时候。

装饰者要求其实现与包装的对象统一,并做到过程透明,意味着可以用他来包装其他对象,而使用方法与原来一致。

一次逻辑的执行可以包含多个装饰对象,这里举个例子来说,在webapp中每个页面的view往往会包含一个show方法,而在我们的页面中我们可能会根据localsorage或者ua判断要不要显示下面广告条,效果如下:

那么这个逻辑应该如何实现呢?

 1 var View = _.inherit({
 2     initialize: function () {},
 3     show: function () {
 4         console.log('渲染基本页面');
 5     }
 6 });
 7 
 8 //广告装饰者
 9 var AdDecorator = _.inherit({
10     initialize: function (view) {
11         this.view = view;
12     },
13     show: function () {
14         this.view.show();
15         console.log('渲染广告区域');
16     }
17 });
18 
19 //基本使用
20 var v = new View();
21 v.show();
22 
23 //........ .满足一定条件...........
24 var d = new AdDecorator(v);
25 d.show();

说实话,就站在前端的角度,以及我的视野来说,这个装饰者其实不太实用,换个说法,这个装饰者模式非常类似面向切口编程,就是在某一个点前做点事情,后做点事情,这个时候事件管道似乎更加合适。

组合模式(composite)

组合模式是前端比较常用的一个模式,目的是解耦复杂程序的内部结构,更业务一点便是将一个复杂组件分成多个小组件,最后保持使用时单个对象和组合对象具有一致性。

假如我这里有一个弹出层容器组件,其内部会有三个select组件,他是这个样子的:

如所见,该组件内部有三个可拖动组件select组件,单独以select的实现便非常复杂,如果一个独立组件要实现该功能便十分让人头疼,外弹出层还设计蒙版等交互,便非常复杂了,那么这个该如何拆分呢?

事实上这里要表达的意思是ui.layer.Container保存着对select组件的依赖,只不过这个UML图是基于强类型语言而出,js并不一定完全一致。

 1 var AbstractView = _.inherit({
 2     initialize: function () {
 3         this.wrapper = 'body'
 4         this.name = '抽象类';
 5     },
 6     show: function () {
 7         console.log('在' + this.wrapper + '中,显示组件:' +  this.name);
 8     }
 9 });
10 
11 //select组件,事实上基础渲染的工作抽象类应该全部做掉
12 var UISelect = _.inherit(AbstractView, {
13     initialize: function ($super) {
14         $super();
15         this.name = 'select组件'
16 //        this.id = '';
17 //        this.value = '';
18         //当前选项
19         this.index = 0;
20         //事实上会根据此数据生产完整组件
21         this.data = [];
22         this.name = 'select组件';
23     }
24 });
25 
26 var UILayerContainer = _.inherit(AbstractView, {
27     initialize: function ($super) {
28         $super();
29         this.name = 'select容器'
30         this.selectArr = [];
31     },
32     add: function(select) {
33         if(select instanceof UISelect) this.selectArr.push(select);
34     },//增加一项
35     remove: function(select){},//移除一项
36     //容器组件显示的同时,需要将包含对象显示
37     show: function ($super) {
38         $super();
39         for(var i = 0, len = this.selectArr.length; i < len; i++){
40             this.selectArr[i].wrapper = this.name;
41             this.selectArr[i].show();
42         }
43     }
44 });
45 
46 var s1 = new UISelect();
47 var s2 = new UISelect();
48 
49 var c = new UILayerContainer();
50 c.add(s1);
51 c.add(s2);
52 
53 c.show();
54 /*
55  在body中,显示组件:select容器 01.html:113
56  在select容器中,显示组件:select组件
57  在select容器中,显示组件:select组件
58  */

怎么说呢,真实的使用场景肯定会有所不同,我们不会在容器外实例化select组件,而是直接在其内部完成;组合模式在工作中是比较常用的,而且容器组件未必会有add,remove等实现,往往只是要求你初始化时能将其内部组件显示好就行。

门面模式(facade)

门面模式又称为外观模式,旨在为子系统提供一致的界面,门面模式提供一个高层的接口,这个接口使得子系统更加容易使用;如果没有外观模式用户便会直接调用子系统,那么用户必须知道子系统更多的细节,而可能造成麻烦与不便。

我对该模式比较印象深刻是由于一次框架的误用,当时做Hybrid开发时,在手机App中嵌入H5程序,通过js调用native接口发生通信,从而突破浏览器限制。

如图所示,当时的想法是,所有业务同事使用native api全部走框架提供的facade层,而不用去关心真实底层的实现,但是当时有点过度设计,做出来的门面有点“太多了”,这是我当时老大的一段代码:

 1 var prototype = require('prototype');
 2 
 3 var UrlSchemeFacade = prototype.Class.create({
 4 
 5   nativeInterfaceMap: {
 6     'geo.locate': 'ctrip://wireless/geo/locate',
 7     'device.info': 'ctrip://wireless/device/info'
 8   },
 9 
10   getUrlScheme: function(key) {
11     return this.nativeInterfaceMap[key];
12   }
13 
14 });
15 
16 UrlSchemeFacade.API = {
17   'GEOLOCATE':'geo.locate',
18   'DEVICEINFO': 'device.info'
19 }
20 
21 var HybridBridge = prototype.Class.create({
22 
23   initialize: function(facade) {
24     this.urlSchemeFacade = facade;
25   },
26 
27   request: function(api) {
28     var url = this.urlSchemeFacade.getUrlScheme(api);
29     console.log(url);
30 
31     // @todo 调用url scheme
32     // window.location.replace = url;
33   }
34 
35 });
36 
37 var Main = function () {
38   var urlSchemeFacade = new UrlSchemeFacade();
39   var hybridBridge = new HybridBridge(urlSchemeFacade);
40 
41   hybridBridge.request(UrlSchemeFacade.API.GEOLOCATE);
42 }
43 
44 Main();

如所示,这里存在一个与native方法api的一个映射,这个意味着我们为每一个方法提供了一个门面?而我们并不知道native会提供多少方法,于是native一旦新增api,我们的门面方法也需要新增,这个是不正确的。

好的做法是应该是像封装Ajax,或者封装addEventListener一样,门面需要提供,但是不应该细化到接口,想象一下,如果我们对所有的事件类型如果都提供门面,那么这个门面该有多难用。

如图所示,真正的门面不应该包含getAddress这一层,而应该将之作为参数传入,代码如:

 1 window.Hybrid = {};
 2 
 3 //封装统一的发送url接口,解决ios、android兼容问题,这里发出的url会被拦截,会获取其中参数,比如:
 4 //这里会获取getAdressList参数,调用native接口回去通讯录数据,形成json data数据,拿到webview的window执行,window.Hybrid['hybrid12334'](data)
 5 var bridgePostMessage = function (url) {
 6     if (isIOS()) {
 7         window.location = url;
 8     } if (isAndriond()) {
 9         var ifr = $('<iframe src="' + url + '"/>');
10         $('body').append(ifr);
11     }
12 };
13 
14 //根据参数返回满足Hybrid条件的url,比如taobao://getAdressList?callback=hybrid12334
15 var _getHybridUrl = function (params) {
16     var url = '';
17     //...aa操作paramss生成url
18     return url;
19 };
20 
21 //页面级用户调用的方法
22 var HybridFacadeRequest = function (params) {
23     //其它操作......
24 
25     //生成唯一执行函数,执行后销毁
26     var t = 'hybrid_' + (new Date().getTime());
27     //处理有回调的情况
28     if (params.callback) {
29         window.Hybrid[t] = function (data) {
30             params.callback(data);
31             delete window.Hybrid[t];
32         }
33     }
34 
35     bridgePostMessage(_getHybridUrl(params))
36 };
37 
38 //h5页面开发,调用Hybrid接口,获取通讯录数据
39 define([], function () {
40     return function () {
41         //业务实际调用点
42         HybridFacadeRequest({
43             //native标志位
44             tagname: 'getAdressList',
45             //返回后执行回调函数
46             callback: function (data) {
47                 //处理data,生成html结构,装载页面
48             }
49         });
50     }
51 });

封装调用子系统的实现,但是不喜欢映射到最终的api,这里不对请您拍砖。

适配器模式(adapter)

适配器模式的目的是将一类接口转换为用户希望的另外一种接口,使原本不兼容的接口可以一起工作。

事实上这种模式一旦使用可能就面临第三方或者其它模块要与你的模块一起使用的需求发生了,这个在.net的数据访问模块dataAdapter也在使用。

这个模式一般是这么个情况,比如最初我们使用的是自己的loading组件,但是现在出了一个情感化loading组件,而这个组件是由其它团队提供,接口与我们完全不一致,这个时候便需要适配器模式的出现了。

如图示,虽然loading组件与情感化loading组件的接口完全不一致,但是他们必须是干的一件事情,如果干的事情也不一样,那么就完不了了......

 1 var UILoading = _.inherit({
 2     initialize: function () {
 3         console.log('初始化loading组件dom结构')
 4     },
 5     show: function () {
 6         console.log('显示loading组件');
 7     }
 8 });
 9 
10 var EmotionLoading = function() {
11     console.log('初始化情感化组件');
12 };
13 EmotionLoading.prototype.init = function () {
14     console.log('显示情感化组件');
15 };
16 
17 var LoadingAdapter = _.inherit(UILoading, {
18     initialize: function (loading) {
19         this.loading = loading;
20     },
21     show: function () {
22         this.loading.init();
23     }
24 })
25 
26 var l1 = new UILoading();
27 l1.show();
28 
29 var l2 = new LoadingAdapter(new EmotionLoading());
30 l2.show();
31 
32 /*
33  初始化loading组件dom结构 01.html:110
34  显示loading组件 01.html:113
35  初始化情感化组件 01.html:118
36  显示情感化组件 
37  */

代理模式(proxy)

代理模式便是棒别人做事,为其他对象提供一种代理,以控制对这个对象的访问,最经典的用法便是:

$.proxy(function() {}, this);

可以看到,最终做的人,依旧是自己,只不过别人以为是代理对象做的,这个有点类似于为人作嫁衣;当然,也可以理解为做替罪羔羊......

所以,代理模式的出现可能是这个对象不方便干一个事情,或者不愿意干,这个时候便会出现中间人了。

比如,我现在是一个博主,我想看我博客的人都点击一下推荐,推荐按钮便是真实对象,现在可能各位不愿意点,而只想看看就走,这个时候如果文档document作为代理者的话,如果用户点击了body部分,便会偷偷的将推荐点了,这便是一种神不知鬼不觉的代理。

这里有三个角色:用户,推荐按钮,body,由于用户只是触发了click事件,这里直接以全局点击事件代替。

1 $('#up').on('click', function() {
2     console.log('推荐');
3 })
4 $('body').on('mousemove', function () {
5     $('#up').click();
6 })

推荐的工作本来是由up按钮对象点击触发的,但是这里却委托了body对象执行;以$.proxy而言,其意义就是里面干的事情全部是代理者(this)干的

再换个说法,如果我们现在有一个按钮组,我们为每一个按钮注册事件似乎有点吃亏了,于是便将实际的执行逻辑交给其父标签

 1 var parent = $('#parent');
 2 for(var i = 0; i < 10; i++){
 3     parent.append($('<input type="button" value="按钮_' + i +'" >'));
 4 }
 5 function itemFn () {
 6     console.log(this.val());
 7 }
 8 parent.on('click', function(e) {
 9     var el = $(e.target);
10     itemFn.call(el);
11 });

父元素代理了子元素的点击事件,但是子元素回调中的this依旧是点击元素,这个便是代理。

观察者模式(observer)

观察者是前端最为经典的模式,又称发布订阅模式,他定义一个一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象状态改变时,或者触发了某一动作,便会通知所有被观察者作出改变动作以更新自身。

累了,这里开始盗百度百科图了:

如所示,主题对象会提供一个类似on接口用以添加观察者,也会给予一个类似off接口移除观察者,适用范围可以是不同对象间,也可以是自身,比如model改变了会通知所有监听model的view做改变。

 1 var Model = _.inherit({
 2     initialize: function (opts) {
 3         this.title = '标题';
 4         this.message = '消息';
 5         this.observes = [];
 6         _.extend(this, opts);
 7     },
 8     on: function(view) {
 9         this.observes.push(view);
10     },
11     off: function() {
12         //略......
13     },
14     //overrid
15     getviewmodel: function () {
16       return { title: this.title, message: this.message };
17     },
18     notify: function () {
19       for(var i = 0, len = this.observes.length; i < len; i++) {
20           this.observes[i].update(this.getviewmodel());
21       }
22     },
23     update: function(title, msg){
24         this.title = title;
25         this.message = msg;
26         this.notify();
27     }
28 });
29 
30 var View = _.inherit({
31     initialize: function (opts) {
32         this.template = '';
33         this.data = {};
34         this.wrapper = $('body');
35         this.$root = $('<div style="display: none;"></div>');
36         _.extend(this, opts);
37     },
38     show: function () {
39         this.$root.html(this.render(this.data));
40         this.wrapper.append(this.$root);
41         this.$root.show();
42     },
43     render: function (data) {
44       return _.template(this.template)(data);
45     },
46     update: function(data) {
47         this.$root.html(this.render(data));
48     }
49 });
50 
51 var model = new Model();
52 
53 var v1 = new View({
54     template: '<div><%=title%></div><div><%=message%></div>',
55     data:  model.getviewmodel()
56 });
57 
58 var v2 = new View({
59     template: '<input value="<%=title%>"><input value="<%=message%>">',
60     data: model.getviewmodel()
61 });
62 
63 model.on(v1);
64 model.on(v2);
65 
66 v1.show();
67 v2.show();
68 
69 setTimeout(function () {
70     model.update('1111', '2222');
71 }, 3000);

这里view首次实例化后,一旦model数据发生变化,两个view会发生变化。

PS:这里的model与view的实现不好,他们不应该主动发生关系,应该有一个viewController负责这些东西,这里是说观察者便不多说。

结语

这次回顾了工作中一些与设计模式相关的内容,有不足请您指出,一些没太用到的便暂时略过了。

微博求粉

posted on 2015-02-15 14:00  叶小钗  阅读(11232)  评论(13编辑  收藏  举报