观察者模式与发布/订阅模式学习

观察者设计模式定义了对象间的一种一对多的组合关系,以便一个对象的状态发生变化时,所有依赖于它的对象都得到通知并自动刷新。
此种模式通常被用来实现事件处理系统。
关于 观察者模式和发布订阅模式可参考链接 https://www.cnblogs.com/lovesong/p/5272752.html
观察者模式定义了四种角色:抽象主题、具体主题、抽象观察者、具体观察者。
1.抽象主题(ISubject): 该角色是一个抽象类或接口,定义了增加、删除、通知观察者对象的方法。
2.具体主题(Subject): 该角色继承或实现了抽象主题,定义了一个集合存入注册过的具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
3.抽象观察者(IObserver): 该角色是具体观察者的抽象类,定义了一个更新方法。
4.具体观察者(Observer): 该角色是具体的观察者对象,在得到具体主题更改通知时更新自身状态。
实际项目中运用的思路如下图
实现代码:
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类中,外面调用时只需声明一个对象,供所有模块调用即可

 
posted @ 2019-02-28 16:09  猪猪快冲  阅读(378)  评论(0编辑  收藏  举报