終於要寫完了~~^_^,期間給同事做了一次培訓,寫一次,講一次的好處是,再次加深了自己對於消息、事件以及觀察者模式的理解。
對我來說,講清楚比寫代碼要難上很多。
這裡分享一些與消息機制相關的一些雜七雜八的內容。
一、可測試的代碼
早些時候,我向銳同學描述我的js程序結構,他問了我一個問題:你的js代碼可測麼?
我蒙了~雖然一直關注敏捷,一直也向往測試驅動開發,但還從沒想過js代碼的可測試(當然,也有測試,但基本上整測加局部測試),沒有想過js的測試驅動。
當時,我遲疑了一會,才說應該是可測的。
寫完上一篇文章(原諒我,覺得太簡單,直接寫的,忘了測試驅動),回頭看了看,還好,基於消息的代碼確實可以做到可測。
比如:
Animal
function Animal(config){ config=config || {}; var othis=this; this.name=config["name"] || "Anonymous"; this.x=config["x"] || 0; var toward=1;//右:1,左-1 var __timeout=0; var __speed=5; //說 this.say=function(str) { this.trigger({type:"say",msg:str}); return str; } //停 this.stop=function() { clearTimeout(__timeout); this.trigger({type:"stop",x:this.x}); return this.x; } //跑動 this.run=function(speed) { __speed=speed || __speed; this.x+=__speed*toward; __timeout=setTimeout(function(){ othis.run();},100); this.trigger({type:"run",x:this.x,toward:toward,speed:__speed}); return {x:this.x,toward:toward,speed:__speed}; } //向左轉 this.turnLeft=function() { toward=-1; this.trigger({type:"turn",toward:toward}); return toward; } //向右轉 this.turnRight=function() { toward=1; this.trigger({type:"turn",toward:toward}); return toward; }}方法都是有返回值,這樣便於我們做一些單元測試,除了單元測試,我們還要做一些基於消息的機制的測試,構造一個偽對象去偵聽Animal對象發送的消息
但這個Animal並不完全,還有一些不可測的,比如toward,它是完全封閉在內部的一個變量,你想要知道Animal的對象前進的方向,就有些困難。
不過這主要源於Animal這個類得不完全,但我們不能為了訪問toward而直接把它修改為this.toward暴露出來,這樣別人有可能賦一個錯誤的值:this.toward=100;仔細看一下代碼,公開toward讓人可以隨別賦值是很危險的一件事情。好的方式,寫一個getToward()方法。
一種難以測試的實現是示例這樣寫的:
Logger
///記錄器function Logger(){ var dom=document.getElementById("log"); var log=function(str) { var time=new Date(); this.dom.innerHTML+="<br/>"+str+'<span style="color:gray">('+time.getHours()+":"+time.getMinutes()+":"+time.getSeconds()+")</span>"; }; this.handler=function(data,p){ switch(data.type) { case "say": this.log(p.name+" 說:"+data.msg); break; case "stop": this.log('<span style="color:green">'+p.name+" 停在了"+data.x+'</span>'); break; case "turn": this.log(p.name+" 轉向了"+(data.toward==1?"右":"左")); break; } }; }Logger對象只暴露了一個handler方法,它寫死了dom。
當然它在示例運行中會很好的執行自己的職責。為了測試它,我們首先保證頁面上要有一個id為log的dom元素,還要偽造一個消息對象,如Animal對象去給它發消息。這讓我們又一種整體測試的感覺。
這不是一個好的示例。
總體而言,消息的密閉性會給測試帶來一些麻煩。實際中,我們一個函數的調用可能會觸發很多個消息,而不只是一個。而這些消息名又灑落在了代碼的各處。除非你仔細的閱讀代碼,否則很可能會遺漏消息。
像Animal一樣,盡量做到一個方法值觸發一個消息,或者相反、相關聯的消息。如果是一些大的對象,把消息名羅列在對象開始前得注釋代碼中,也便於他們維護調試。
二、冒泡的消息
在剛開始的時候,我曾實現過消息的冒泡,就像是內部a標簽的click事件會冒泡到外部div一樣。
消息的冒泡示例:
a.bind("test",b) b.bind("test",c) c.bind("test",d)
如果你實現了冒泡,a的test消息會沿著a->b->c->d的路徑一直傳送到d
簡單的實現呢,就是每個對象的handler函數,直接trigger一下傳進來的消息,這樣的方式並沒有實現自動化。一個簡單改動如下:
View Code
function trigger(Y){ var queue =[]; var qs=this.__MSG_QS__ || {}; var sqs=this.__STATIC_MSG_QS__|| {}; queue= queue.concat(qs[Y.type] || []); queue= queue.concat(sqs[Y.type] || []); for (var a = 0, X = queue.length; a < X; a++) { if(queue[a].handler) { queue[a].handler(Y,this) if(queue[a].trigger) { queue[a].trigger(Y); } } else { queue[a].call(this,Y,this); } } } 重點看一下這一個改動:
if(queue[a].trigger) { queue[a].trigger(Y); }如果發現觀察者是一個monitor模式對象,那麼調用它的trigger
這樣,我們的消息就可以實現冒泡了。我們也可以為對象添加一個屬性,標示對象是否允許冒泡,也可以再添加stopPropagation一個來阻止冒泡。
關於消息的冒泡,我的建議是謹慎使用。自定義對象不同於dom,dom是有層次結構的,dom只對父元素冒泡。
自定義對象是沒有層次的(除非強制),有時我們甚至可以讓對象自己監聽自己的消息,很多時候,有很多對象偵聽你的消息。實質的講,此時的消息流,並不像冒泡,而是會有分支,在處理不好的情況下會有閉環,會有類似遞歸一樣的自調用,自調用、閉環的情況都會引起死循環。
當然,你也可以使用一個字典來存儲消息已經傳遞過的對象(注入到消息體內,或作為trigger另一個參數),防止閉環。但這樣又會使你的monitor代碼增加很多的處理。
三、異步的消息
之前,我所舉的例子、代碼,都是同步消息,消息調用,監聽函數就會執行,並且只有所有的監聽函數執行完畢,trigger的調用才結束。
比如:
obj.trigger({type:"test1"});
obj.trigger({type:"test2"});
test2的消息一定是在test1消息調用結束後才調用。
異步的消息,只是我的一個想法,利用setTimeout函數,用時間片得形式,一次只調用一個監聽者,那麼在消息源對象調用trigger方法,trigger很快可以執行完畢,而真正的消息處理呢,是放在了時間片中,慢慢的處理。
目前我沒有用到需要異步消息處理的需求,對於設想的方案,沒有做過測試。有興趣的人可以自己研究一下。
估計有的js框架有類似的功能,可惜我沒有研究過。
四、避免重復注冊及注銷觀察者
如果你a.bind("test",b)兩次,會發生什麼情況?
a對象的test的事件,會很忠實地調用兩次b。
為了避免重復,直接的方式就是一遍列表,判斷一下b是否存在,這樣的效率很低下。一種方式是為需要bind的對象,增加一個唯一標示,在monitor內增加一個函數(或者在全局增加一個函數),代碼如下
var monitor= (function(){//…… var __guid=0; function guid(){ return ++__guid; } function bind(b){ var queue = this.__MSG_QS__=this.__MSG_QS__ || {}; if (!queue[b]) { queue[b] = {} } for (var a = 1, X = arguments.length, Y; a < X; a++) { var _guid=arguments[a].__guid=arguments[a].__guid || guid(); if( queue[b][_guid]) queue[b][_guid]=arguments[a]; } }//……})();相應的trigger裡也要做一些相應的改變(以及live),這裡不再給出代碼,有興趣的自己實現
注銷是bind的反操作,如果你沒有使用為對象賦唯一標示的方式,你需要用循環去判斷對象是否在觀察者隊列中,如果在則從隊列中移除。如果使用唯一標示,操作比較簡單,使用delete即可。