什么是觀察者模式?
觀察者模式又叫做發布訂閱模式,它定義了一種一對多的關系,讓多個觀察者對象同時監聽某一個主題對象,這個主題對象的狀態發生改變時就會通知所有觀察着對象。它是由兩類對象組成,主題和觀察者,主題負責發布事件,同時觀察者通過訂閱這些事件來觀察該主體,發布者和訂閱者是完全解耦的,彼此不知道對方的存在,兩者僅僅共享一個自定義事件的名稱。
理解觀察者模式:
JS傳統事件就是一個觀察者模式,之所以要有觀察者模式,是因為有時候和傳統事件無關的事件,比如:2個或者更多模塊的直接通信問題,比如說我有個index.html頁面,我有很多JS文件,比如:
a.js: function a(){}; b.js: function b(){}; c.js function c(){}; 等等。后面還有許多這樣的JS,那么我要在index.html初始化這些函數的話,我需要這樣調用a();b();c()等等,也就是說頁面調用的時候 我要這樣調用,增加了依賴性,我要知道有多少個函數要這樣初始化調用,但是如果我們現在用觀察者模式就不需要知道有哪些訂閱者,比如一個模塊(或者多個模塊)訂閱了一個主題(或者事件),另一個模塊發布這個主題時候,訂閱這個主題模塊就可以執行了,觀察者主要讓訂閱者與發布者解耦,發布者不需要知道哪些模塊訂閱了這個主題,它只管發布這個主題就可以了,同樣訂閱者也無需知道那個模塊會發布這個主題,它只管訂閱這個主題就可以了。這樣2個模塊(或更多模塊)就實現了關聯了。而不需要和上面代碼一樣,我要知道哪些模塊要初始化,我要怎樣初始化。這只是一個簡單的列子解釋觀察者模式要使用在什么地方,我也看過很多博客關於這方面的資料,但是很多人寫博客只是講了如何實現觀察者模式及觀察者模式的好處,並沒有講我們什么時候該使用觀察者模式,所以我列舉了上面的列子,就是多個不同業務模塊需要相互關聯的時候,可以使用觀察者模式。就好比requireJS,seaJS,KISSY解決依賴的問題一樣(比如A依賴於B,B依賴於C,只要一個解決入口文件,其他都會異步加載出來一樣)。也就是說各個模塊之間的關聯性可以使用觀察者模式來設計。
這種模式有多種實現,比如jquery插件 pub/sub
比如如下代碼:
jQuery.subscribe(“done”,fun2);
function fun1(){
jQuery.publish(“done”);
}
上面的jQuery.publish(“done”);意思是執行fun1函數后,向信號中心jquery發布done信號,而jquery.subscribe(“done”,fun2)的意思是:綁定done信號,執行fun2函數。
我們還可以看看nodejs核心模塊Events提供EventEmitter對象,也實現了分布式事件。如下代碼:
var Emitter = require('events').EventEmitter;
var emitter = new Emitter();
emitter.on('someEvent',function(stream){
console.log(stream + 'from eventHandler1');
});
emitter.on('someEvent',function(stream){
console.log(stream + 'from eventHandler2');
});
emitter.emit('someEvent','I am a stream!');
上面nodejs的 emitter對象中的 emitter.on是指發布事件”someEvent”,而emitter.emit是指觸發事件,事件名稱為”someEvent”.從而執行回掉函數。在nodeJS中我們可以發布很多事件,事件名稱為someEvent,這樣每一個回掉就實現了一個業務邏輯,這樣代碼耦合性降低了。
我們現在可以實現自己的Pub/Sub模式,代碼如下:
function PubSub() { this.handlers = {}; } PubSub.prototype = { // 訂閱事件 on: function(eventType,handler){ var self = this; if(!(eventType in self.handlers)) { self.handlers[eventType] = []; } self.handlers[eventType].push(handler); return this; }, // 觸發事件(發布事件) emit: function(eventType){ var self = this; var handlerArgs = Array.prototype.slice.call(arguments,1); for(var i = 0; i < self.handlers[eventType].length; i++) { self.handlers[eventType][i].apply(self,handlerArgs); } return self; } };
// 調用方式如下:
var pubsub = new PubSub();
pubsub.on('A',function(data){
console.log(1 + data); // 執行第一個回調業務函數
});
pubsub.on('A',function(data){
console.log(2 + data); // 執行第二個業務回調函數
});
// 觸發事件A
pubsub.emit('A',"我是參數");
二:javascript自定義事件
Javascript傳統事件有 點擊事件(click),鼠標移上去事件(mouseover)等等,那么什么是自定義事件呢?自定義事件可以這樣理解傳統事件沒有的,就好比很多人發明東西一樣,何謂發明?就是世界上沒有的東西,現在被自己做到了,這叫發明,所以我們自定義事件也可以這樣理解---目前傳統事件沒有的。
2. 為什么要自定義事件,自定義事件要使用在地方?
傳統的事件不能滿足我們的需求,所以我們需要自定義事件,比如傳統的事件有單擊,雙擊,但是突然某一天我想要三擊 那就要用到自定義事件了,自定義事件一般使用在觀察者模式上,比如主體需要發布各種消息通過創建各種自定義事件來實現,對於消息的訂閱則通過注冊監聽器來實現。
3. 如何創建自定義事件?
1. 在標准瀏覽下(除IE8及以下) 我們可以如下這樣創建自定義事件.比如如下代碼:
<div id="longen">我來測試</div> var test = document.getElementById("longen"); // 創建事件 var evt = document.createEvent('Event'); // 定義事件類型 evt.initEvent('customEvent',true,true); // 監聽事件 test.addEventListener('customEvent',function(){ console.log("111"); },false); // 觸發事件 test.dispatchEvent(evt);
如上,在標准瀏覽下 運行下 在控制台可以看到 輸入111內容了,說明自定義事件成功觸發,在這個過程中,createEvent方法創建了一個空事件evt,然后使用initEvent方法定義事件的類型為約定好的自定義事件,再對元素進行監聽,最后使用dispatchEvent來觸發事件了。自定義事件無非就是監聽事件,然后自己運行回調函數,上面的initEvent的第二個參數的意思是:是否冒泡,第三個參數的意思是:是否可以使用preventDefault()來阻止默認行為。但是上面的自定義事件只能對標准瀏覽器下生效,IE8及以下都不生效,不支持createEvent()這個方法,所以我們現在需要IE8及以下的事件。在IE下我們可以使用onpropertychange事件來監聽,當DOM的某個屬性發生改變時就觸發onpropertychange事件的回調,再在回調中判斷改變的屬性是否是我們自定義的屬性,假如是則執行我們的回調,否則不執行。
如下在IE8及以下代碼可以實現如下測試:
<div id="longen">我來測試</div> var test = document.getElementById("longen"); document.documentElement.myEvent = 0; function foo(){ alert('已經監聽到了'); } document.documentElement.attachEvent("onpropertychange",function(event) { if (event.propertyName == "myEvent") { foo(); } }); document.documentElement.myEvent++;
如上代碼就可以在IE下自定義成功觸發了。
綜合:我們可以寫一個跨瀏覽器的自定義事件了,代碼如下:
function DefineEvent(element) { this.init(element); } DefineEvent.prototype = { constructor: DefineEvent, init: function(element) { this.element = (element && element.nodeType == 1) ? element : document; return this; }, /* * 添加監聽事件
* @param {string} type 監聽的事件類型 * @param {Function} callback 回調函數 */ addEvent: function(type,callback) { var self = this; if(self.element.addEventListener) { // 標准瀏覽器下 self.element.addEventListener(type,callback,false); }else if(self.element.attachEvent){ // IE if(isNaN(self.element[type])) { self.element[type] = 0; } var fun = function(evt){ evt = evt ? evt : window.event; if(evt.propertyName == type) { callback.call(self.element); } } self.element.attachEvent('onpropertychange',fun); // 在元素上存儲綁定回調,方便移除事件綁定 if(!self.element['callback' + callback]) { self.element['callback' + callback] = fun; } }else { self.element.attachEvent('on' + type,callback); } return self; }, /* * 移除事件 * @param {string} type 監聽的事件類型 * @param {Function} callback 回調函數 */ removeEvent: function(type,callback){ var self = this; if(self.element.removeEventListener) { self.element.removeEventListener(type,callback,false); }else if(self.element.detachEvent) { // 移除對應的自定義屬性監聽 self.element.detachEvent('onpropertychange',self.element['callback' + callback]); // 刪除儲存在 DOM 上的自定義事件的回調 self.element['callback' + callback] = null; }else { self.element.detachEvent('on' + type,callback); } return self; }, /* * 觸發事件 * @param {String} type 觸發事件的類型 * @return {object} 返回的對象 */ triggerEvent: function(type){ var self = this; if(self.element.dispatchEvent) { // 標准瀏覽器下 // 創建事件 var evt = document.createEvent('Event'); // 定義事件的類型 evt.initEvent(type,true,true); // 觸發事件 self.element.dispatchEvent(evt); }else if(self.element.fireEvent) { // IE self.element[type]++; } return self; } };
HTML
<div id="longen">我來測試</div>
調用如下:
var testBox = document.getElementById('longen');
var defineEvent = new DefineEvent(testBox);
// 回調函數1
function triggerEvent(){
console.log('觸發了一次自定義事件 customConsole');
}
// 回調函數2
function triggerAgain(){
console.log('再一次觸發了自定義事件 customConsole');
}
// 同時綁定兩個回調函數,支持鏈式調用
defineEvent.addEvent('aa', triggerEvent).addEvent('aa', triggerAgain);
defineEvent.triggerEvent('customConsole');
我們可以在控制台看到已經輸出來了2條信息。我們也可以對某個自定義函數進行移除操作,比如如下:
defineEvent.removeEvent('aa',triggerAgain);
defineEvent.triggerEvent('aa');
我對triggerAgain函數進行移除,可以看到就不會這個函數的信息了。