觀察者模式與發佈/訂閱模式學習

觀察者設計模式定義了對象間的一種一對多的組合關係,以便一個對象的狀態發生變化時,全部依賴於它的對象都獲得通知並自動刷新。
此種模式一般被用來實現事件處理系統。
關於 觀察者模式和發佈訂閱模式可參考連接 http://www.javashuo.com/article/p-ycyepdfv-hs.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)html

  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.tsapp

 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.tsdom

  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類中,外面調用時只需聲明一個對象,供全部模塊調用便可測試

相關文章
相關標籤/搜索