观察者模式与发布/订阅模式学习
观察者设计模式定义了对象间的一种一对多的组合关系,以便一个对象的状态发生变化时,所有依赖于它的对象都得到通知并自动刷新。
此种模式通常被用来实现事件处理系统。
关于 观察者模式和发布订阅模式可参考链接 https://www.cnblogs.com/lovesong/p/5272752.html
观察者模式定义了四种角色:抽象主题、具体主题、抽象观察者、具体观察者。
1.抽象主题(ISubject): 该角色是一个抽象类或接口,定义了增加、删除、通知观察者对象的方法。
2.具体主题(Subject): 该角色继承或实现了抽象主题,定义了一个集合存入注册过的具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
3.抽象观察者(IObserver): 该角色是具体观察者的抽象类,定义了一个更新方法。
4.具体观察者(Observer): 该角色是具体的观察者对象,在得到具体主题更改通知时更新自身状态。
实际项目中运用的思路如下图
![](https://img2018.cnblogs.com/blog/1413079/201902/1413079-20190228105028633-1732819506.png)
实现代码:
1.定义抽象主题(ISubject)
1 namespace ob { 2 /** 3 * 主题集合对象,发起通知器 4 */ 5 export interface ISubject { 6 7 /** 8 * 添加一个观察者通知 9 * @param notic 通知主题 10 * @param listener 通知监听回调函数 11 * @param thisObj 通知回调函数的this对象 12 */ 13 on(notic:string|number, listener:Function, thisObj:Object); 14 15 /** 16 * 添加一次性观察者通知 17 * @param notic 通知主题 18 * @param listener 通知监听回调函数 19 * @param thisObj 通知回调函数的this对象 20 */ 21 once(notic:string|number, listener:Function, thisObj:Object); 22 23 /** 24 * 发起一个通知 25 * @param notic 通知主题 26 * @param args 携带的参数 27 */ 28 send(notic:string|number, ...args:any[]):void; 29 30 /** 31 * 删除一个观察者通知 32 * @param notic 通知主题 33 * @param listener 通知监听回调函数 34 */ 35 off(notic:string|number, listener:Function,thisObj:Object); 36 } 37 }
2.具体实现主题(Subject)
1 namespace ob { 2 /** 3 * 主题集合对象,发起通知器,代替派发事件 4 */ 5 export class Subject implements ISubject{ 6 7 private funcMap:HashMap<string|number, NoticeList>;//根据主题存放监听事件对象 8 9 constructor(){ 10 this.funcMap = new HashMap<string|number, NoticeList>(); 11 } 12 13 /** 14 * 添加一个观察者通知 15 * @param notic 通知主题 16 * @param listener 通知监听回调函数 17 * @param thisObj 通知回调函数的this对象 18 */ 19 on(notice:string|number, listener:Function, thisObj:Object):void{ 20 this.addNoticeData(notice, listener, thisObj); 21 } 22 23 /** 24 * 添加一次性观察者通知 25 * @param notic 通知主题 26 * @param listener 通知监听回调函数 27 * @param thisObj 通知回调函数的this对象 28 */ 29 once(notice:string|number, listener:Function, thisObj:Object):void{ 30 this.addNoticeData(notice, listener, thisObj, true); 31 } 32 33 /** 34 * 删除一个观察者通知 35 * @param notic 通知主题 36 * @param listener 通知监听回调函数 37 */ 38 off(notice:string|number, listener:Function,thisObj:Object):void{ 39 let listObj = this.funcMap.get(notice); 40 if(listObj){ 41 let dataList = listObj.dataList; 42 //是否正在遍历,如果是,则进行复制 43 if(listObj.isRunning){ 44 listObj.dataList = dataList = dataList.concat(); 45 } 46 let data:NoticeData; 47 for(let i=0,len=dataList.length;i<len;i++){ 48 data = dataList[i]; 49 if(data.listener==listener && data.thisObj==thisObj){ 50 dataList.splice(i,1);//在注册的时候已经保证不会有重复的,所以找到一个就删除返回 51 return; 52 } 53 } 54 } 55 } 56 57 /** 58 * 发起一个通知 59 * @param notic 通知主题 60 * @param args 携带的参数 61 */ 62 send(notice:string|number, ...args:any[]):void{ 63 let listObj = this.funcMap.get(notice); 64 if(listObj){ 65 let onceList:NoticeData[] = []; 66 listObj.isRunning = true; 67 for(let data of listObj.dataList){ 68 data.listener.apply(data.thisObj, args); 69 if(data.isOnce){ 70 onceList.push(data); 71 } 72 } 73 listObj.isRunning = false; 74 while(onceList.length){ 75 let data = onceList.pop(); 76 this.off(notice, data.listener, data.thisObj); 77 } 78 if(listObj.dataList.length<1){//移除监听 79 this.funcMap.remove(notice); 80 } 81 } 82 } 83 84 85 /** 具体实现添加主题事件的方法 */ 86 private addNoticeData(notice:string|number, listener:Function, thisObj:Object, isOnce?:boolean):void{ 87 if(!listener || !thisObj){ 88 throw new Error("listener、thisObj均不能为空");//抛出错误可阻止后面代码运行 89 } 90 let listObj = this.funcMap.get(notice);//由事件主题获取对应的方法集合 91 if(!listObj){ 92 //这里也可以考虑使用对象池 93 listObj = new NoticeList(); 94 this.funcMap.put(notice, listObj); 95 } 96 let dataList = listObj.dataList;//该主题下所有存放的事件数据 97 for(let data of dataList){//去重处理,不会有重复的事件被添加 98 //必须监听方法和this相等才能决定是同一个监听 99 if(data.listener==listener && data.thisObj==thisObj){ 100 return; 101 } 102 } 103 //判断该主题事件是否正在遍历中 104 if(listObj.isRunning){ 105 //为了不影响正常的遍历,复制出一个数组来保存数据,那么遍历完的数组就会失去引用,从而被垃圾回收 106 listObj.dataList = dataList = dataList.concat(); 107 } 108 //开始实例化出一个事件对象 109 let obj = new NoticeData(listener, thisObj, isOnce); 110 dataList.push(obj); 111 } 112 } 113 }
在第2步中,需要用到几个类,NoticeData(存放主题数据),NoticeList(存放数据数组和遍历状态),第三个HashMap是工具来的,用对象Object封装成了常用的键值对像,具体代码如下
NoticeData.ts
1 namespace ob { 2 export class NoticeData { 3 /** 通知主题 (暂时没用到)**/ 4 notice:string | number; 5 /** 监听函数 **/ 6 listener:Function; 7 /** 执行域对象 **/ 8 thisObj:Object; 9 /** 是否只执行一次就删除 **/ 10 isOnce:boolean; 11 12 public constructor(listener:Function, thisObj:Object, isOnce?:boolean) { 13 this.listener = listener; 14 this.thisObj = thisObj; 15 this.isOnce = isOnce; 16 } 17 } 18 }
NoticeList.ts
1 namespace ob { 2 export class NoticeList { 3 4 /** 存放数据的数组*/ 5 dataList:NoticeData[]; 6 7 /** 是否正在遍历,true为正在遍历 */ 8 isRunning:boolean; 9 10 public constructor() { 11 this.dataList = []; 12 } 13 } 14 }
工具类 HashMap.ts
1 /** 2 * 用Object保存键值对,所以K应为简单数据类型,比如number,string,其他的类型Object会自动将其转化为string类型 3 */ 4 class HashMap<K,V> { 5 6 private obj:Object;//存放数据的对象 7 private length:number; 8 9 public constructor() { 10 this.obj = {}; 11 this.length = 0; 12 } 13 14 /** 15 * 将指定的值与此映射中的指定键相关联. 16 * @param key 与指定值相关联的键.key的类型应为number|string,其他类型会被冲掉 17 * @param value 与指定键相关联的值. 18 */ 19 put(key:K, value:V):void{ 20 let obj = this.obj; 21 if(obj[key as any]){//该key不存在对应的值时,长度+1 22 this.length++; 23 } 24 obj[key as any] = value; 25 } 26 27 /** 28 * 返回此映射中映射到指定键的值. 29 * @param key 与指定值相关联的键. 30 * @return 此映射中映射到指定值的键,如果此映射不包含该键的映射关系,则返回 undefined (或者 null). 31 */ 32 get(key:K):V{ 33 return this.obj[key as any]; 34 } 35 36 /** 37 * 获取此映射的长度 38 */ 39 size():number{ 40 return this.length; 41 } 42 43 /** 44 * 是否为空 45 */ 46 isEmpty():boolean{ 47 return this.length<1; 48 } 49 50 /** 51 * 如果此映射包含指定键的映射关系,则返回 true. 52 * @param key 测试在此映射中是否存在的键. 53 * @return 如果此映射包含指定键的映射关系,则返回 true. 54 */ 55 hasKey(key:K):boolean{ 56 if(this.obj[key as any]){ 57 return true; 58 } 59 return false; 60 } 61 62 /** 63 * 返回此映射中包含的所有key值. 64 * @return 包含所有key的数组 65 */ 66 keys():K[]{ 67 let arr:K[] = []; 68 if(this.length>0){ 69 let obj = this.obj; 70 for(let key in obj){ 71 arr.push(key as any); 72 } 73 } 74 return arr; 75 } 76 77 /** 78 * 返回此映射中包含的所有value值. 79 * @return 包含所有value的数组 80 */ 81 values():V[]{ 82 let arr:V[] = []; 83 if(this.length>0){ 84 let obj = this.obj; 85 for(let key in obj){ 86 arr.push(obj[key]); 87 } 88 } 89 return arr; 90 } 91 92 /** 93 * 遍历所有映射均执行方法,方法携带的参数只包括映射的值 94 */ 95 forEach(func:Function, thisObj:Object):void{ 96 let obj = this.obj; 97 for(let key in obj){ 98 func.call(thisObj, obj[key]); 99 } 100 } 101 102 /** 103 * 遍历所有映射均执行方法,方法携带的参数包括映射的key, value 104 */ 105 forKeyValue(func:Function, thisObj):void{ 106 let obj = this.obj; 107 for(let key in obj){ 108 func.call(thisObj, key, obj[key]); 109 } 110 } 111 112 /** 113 * 删除并返回此映射中映射到指定键的值. 114 * @param key 与指定值相关联的键. 115 * @return 返回此映射中映射到指定值的键,如果此映射不包含该键的映射关系,则返回 undefined (或者 null). 116 */ 117 remove(key:K):V{ 118 let value = this.obj[key as any]; 119 if(value){ 120 delete this.obj[key as any]; 121 this.length--; 122 } 123 return value; 124 } 125 126 /** 127 * 清除所有映射 128 */ 129 clear(){ 130 this.length = 0; 131 let obj = this.obj; 132 for(let key in obj){ 133 delete obj[key]; 134 } 135 } 136 }
在项目中,将Subject进行了二次封装,类似于单例,大家共用一个对象
下面是一个简单的示例:
1 class Test extends egret.DisplayObjectContainer { 2 public constructor() { 3 super(); 4 this.addEventListener(egret.Event.ADDED_TO_STAGE, this.onAddToStage, this); 5 } 6 7 private onAddToStage(e: egret.Event): void { 8 let txt1 = new egret.TextField(); 9 txt1.size = 50; 10 txt1.text = "这里是一次性事件变化的显示内容"; 11 this.addChild(txt1); 12 13 let txt2 = new egret.TextField(); 14 txt2.size = 50; 15 txt2.text = "这里是通用事件变化的显示区域"; 16 txt2.y = 100; 17 this.addChild(txt2); 18 19 let subject = new ob.Subject(); 20 subject.once("123", (p1,p2)=>{ 21 console.log("p1="+p1," p2="+p2); 22 txt1.textColor = 0xfff000; 23 },this); 24 25 let cout = 0; 26 subject.on("123", testFun, this); 27 function testFun(p1,p2){ 28 console.log("p1="+p1," p2="+p2); 29 txt2.textColor = Math.random() * 0xffffff; 30 cout++; 31 if(cout>10){ 32 subject.off("123", testFun, this); 33 } 34 } 35 36 this.stage.addEventListener(egret.TouchEvent.TOUCH_TAP, (e:egret.TouchEvent)=>{ 37 console.log("点了面板") 38 subject.send("123","参数1","参数2"); 39 }, this); 40 41 } 42 43 }
总结:在项目中,类似这种模式代替了事件机制,里面的逻辑部分就在Subject类中,外面调用时只需声明一个对象,供所有模块调用即可