什麼是觀察者模式?
觀察者模式又叫做釋出訂閱模式,它定義了一種一對多的關係,讓多個觀察者物件同時監聽某一個主題物件,這個主題物件的狀態發生改變時就會通知所有觀察著物件。它是由兩類物件組成,主題和觀察者,主題負責釋出事件,同時觀察者通過訂閱這些事件來觀察該主體,釋出者和訂閱者是完全解耦的,彼此不知道對方的存在,兩者僅僅共享一個自定義事件的名稱。
理解觀察者模式:
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函式進行移除,可以看到就不會這個函式的資訊了。