javascript
javascript-发布订阅模式
說明:本篇文章轉(zhuǎn)載自小火柴的藍(lán)色理想的一篇博文。原文地址:http://www.cnblogs.com/xiaohuochai/p/8031564.html
?
發(fā)布—訂閱模式又叫觀察者模式,它定義對(duì)象間的一對(duì)多的依賴關(guān)系,當(dāng)一個(gè)對(duì)象的狀態(tài)發(fā)生改變時(shí),所有依賴于它的對(duì)象都將得到通知。在javascript開發(fā)中,一般用事件模型來替代傳統(tǒng)的發(fā)布—訂閱模式。本文將詳細(xì)介紹發(fā)布訂閱模式
現(xiàn)實(shí)事例
不論是在程序世界里還是現(xiàn)實(shí)生活中,發(fā)布—訂閱模式的應(yīng)用都非常廣泛
比如,小明最近看上了一套房子,到了售樓處之后才被告知,該樓盤的房子早已售罄。好在售樓處工作人員告訴小明,不久后還有一些尾盤推出,開發(fā)商正在辦理相關(guān)手續(xù),手續(xù)辦好后便可以購買。但到底是什么時(shí)候,目前還沒有人能夠知道。于是小明記下了售樓處的電話,以后每天都會(huì)打電話過去詢問是不是已經(jīng)到了購買時(shí)間。除了小明,還有小紅、小強(qiáng)、小龍也會(huì)每天向售樓處咨詢這個(gè)問題。一個(gè)星期過后,該工作人員決定辭職,因?yàn)閰捑肓嗣刻旎卮?000個(gè)相同內(nèi)容的電話
當(dāng)然現(xiàn)實(shí)中沒有這么笨的銷售公司,實(shí)際上故事是這樣的:小明離開之前,把電話號(hào)碼留在了售樓處。售樓處工作人員答應(yīng)他,新樓盤一推出就馬上發(fā)信息通知小明。小紅、小強(qiáng)和小龍也是一樣,他們的電話號(hào)碼都被記在售樓處的花名冊(cè)上,新樓盤推出的時(shí)候,售樓處工作人員會(huì)翻開花名冊(cè),遍歷上面的電話號(hào)碼,依次發(fā)送一條短信來通知他們
在上面的例子中,發(fā)送短信通知就是一個(gè)典型的發(fā)布—訂閱模式,小明、小紅等購買者都是訂閱者,他們訂閱了房子開售的消息。售樓處作為發(fā)布者,會(huì)在合適的時(shí)候遍歷花名冊(cè)上的電話號(hào)碼,依次給購房者發(fā)布消息
使用發(fā)布—訂閱模式有著顯而易見的優(yōu)點(diǎn):購房者不用再天天給售樓處打電話咨詢開售時(shí)間,在合適的時(shí)間點(diǎn),售樓處作為發(fā)布者會(huì)通知這些消息訂閱者;購房者和售樓處之間不再強(qiáng)耦合在一起,當(dāng)有新的購房者出現(xiàn)時(shí),他只需把手機(jī)號(hào)碼留在售樓處,售樓處不關(guān)心購房者的任何情況,不管購房者是男是女還是一只猴子。而售樓處的任何變動(dòng)也不會(huì)影響購買者,比如售樓處工作人員離職,售樓處從一樓搬到二樓,這些改變都跟購房者無關(guān),只要售樓處記得發(fā)短信這件事情
?
DOM事件
發(fā)布—訂閱模式可以廣泛應(yīng)用于異步編程中,這是一種替代傳遞回調(diào)函數(shù)的方案。比如,可以訂閱ajax請(qǐng)求的error、succ等事件。或者如果想在動(dòng)畫的每一幀完成之后做一些事情,可以訂閱一個(gè)事件,然后在動(dòng)畫的每一幀完成之后發(fā)布這個(gè)事件。在異步編程中使用發(fā)布—訂閱模式,就無需過多關(guān)注對(duì)象在異步運(yùn)行期間的內(nèi)部狀態(tài),而只需要訂閱感興趣的事件發(fā)生點(diǎn)
發(fā)布—訂閱模式可以取代對(duì)象之間硬編碼的通知機(jī)制,一個(gè)對(duì)象不用再顯式地調(diào)用另外一個(gè)對(duì)象的某個(gè)接口。發(fā)布—訂閱模式讓兩個(gè)對(duì)象松耦合地聯(lián)系在一起,雖然不太清楚彼此的細(xì)節(jié),但這不影響它們之間相互通信。當(dāng)有新的訂閱者出現(xiàn)時(shí),發(fā)布者的代碼不需要任何修改;同樣發(fā)布者需要改變時(shí),也不會(huì)影響到之前的訂閱者。只要之前約定的事件名沒有變化,就可以自由地改變它們
實(shí)際上,只要在DOM節(jié)點(diǎn)上面綁定過事件函數(shù),那就使用過發(fā)布—訂閱模式
document.body.addEventListener('click',function(){alert(2); },false); document.body.click(); //模擬用戶點(diǎn)擊在這里需要監(jiān)控用戶點(diǎn)擊document.body的動(dòng)作,但是沒辦法預(yù)知用戶將在什么時(shí)候點(diǎn)擊。所以訂閱document.body上的click事件,當(dāng)body節(jié)點(diǎn)被點(diǎn)擊時(shí),body節(jié)點(diǎn)便會(huì)向訂閱者發(fā)布這個(gè)消息
當(dāng)然還可以隨意增加或者刪除訂閱者,增加任何訂閱者都不會(huì)影響發(fā)布者代碼的編寫
document.body.addEventListener('click',function(){alert(2); },false); document.body.addEventListener('click',function(){ alert(3); },false); document.body.addEventListener('click',function(){ alert(4); },false); document.body.click(); //模擬用戶點(diǎn)擊[注意]手動(dòng)觸發(fā)事件更好的做法是IE下用fireEvent,標(biāo)準(zhǔn)瀏覽器下用dispatchEvent實(shí)現(xiàn)
?
自定義事件
除了DOM事件,還會(huì)經(jīng)常實(shí)現(xiàn)一些自定義的事件,這種依靠自定義事件完成的發(fā)布—訂閱模式可以用于任何javascript代碼中
下面是實(shí)現(xiàn)發(fā)布—訂閱模式的步驟:
1、先要指定好誰充當(dāng)發(fā)布者(比如售樓處)
2、然后給發(fā)布者添加一個(gè)緩存列表,用于存放回調(diào)函數(shù)以便通知訂閱者(售樓處的花名冊(cè))
3、最后發(fā)布消息的時(shí)候,發(fā)布者會(huì)遍歷這個(gè)緩存列表,依次觸發(fā)里面存放的訂閱者回調(diào)函數(shù)(遍歷花名冊(cè),挨個(gè)發(fā)短信)
另外,還可以往回調(diào)函數(shù)里填入一些參數(shù),訂閱者可以接收這些參數(shù)。這是很有必要的,比如售樓處可以在發(fā)給訂閱者的短信里加上房子的單價(jià)、面積、容積率等信息,訂閱者接收到這些信息之后可以進(jìn)行各自的處理
var salesOffices = {}; // 定義售樓處 salesOffices.clientList = []; // 緩存列表,存放訂閱者的回調(diào)函數(shù) salesOffices.listen = function( fn ){ // 增加訂閱者 this.clientList.push( fn ); // 訂閱的消息添加進(jìn)緩存列表 }; salesOffices.trigger = function(){ // 發(fā)布消息 for( var i = 0, fn; fn = this.clientList[ i++ ]; ){ fn.apply( this, arguments ); // (2) // arguments 是發(fā)布消息時(shí)帶上的參數(shù) } }; salesOffices.listen( function( price, squareMeter ){ // 小明訂閱消息 console.log( '價(jià)格= ' + price ); console.log( 'squareMeter= ' + squareMeter ); }); salesOffices.listen( function( price, squareMeter ){ // 小紅訂閱消息 console.log( '價(jià)格= ' + price ); console.log( 'squareMeter= ' + squareMeter ); }); salesOffices.trigger( 2000000, 88 ); // 輸出:200 萬,88 平方米 salesOffices.trigger( 3000000, 110 ); // 輸出:300 萬,110 平方米至此,已經(jīng)實(shí)現(xiàn)了一個(gè)最簡(jiǎn)單的發(fā)布—訂閱模式,但這里還存在一些問題。看到訂閱者接收到了發(fā)布者發(fā)布的每個(gè)消息,雖然小明只想買88平方米的房子,但是發(fā)布者把110平方米的信息也推送給了小明,這對(duì)小明來說是不必要的困擾。所以有必要增加一個(gè)標(biāo)示key,讓訂閱者只訂閱自己感興趣的消息。改寫后的代碼如下:
var salesOffices = {}; // 定義售樓處 salesOffices.clientList = []; // 緩存列表,存放訂閱者的回調(diào)函數(shù) salesOffices.listen = function( key, fn ){ if ( !this.clientList[ key ] ){ // 如果還沒有訂閱過此類消息,給該類消息創(chuàng)建一個(gè)緩存列表 this.clientList[ key ] = []; } this.clientList[ key ].push( fn ); // 訂閱的消息添加進(jìn)消息緩存列表 }; salesOffices.trigger = function(){ // 發(fā)布消息 var key = Array.prototype.shift.call( arguments ), // 取出消息類型 fns = this.clientList[ key ]; // 取出該消息對(duì)應(yīng)的回調(diào)函數(shù)集合 if ( !fns || fns.length === 0 ){ // 如果沒有訂閱該消息,則返回 return false; } for( var i = 0, fn; fn = fns[ i++ ]; ){ fn.apply( this, arguments ); // (2) // arguments 是發(fā)布消息時(shí)附送的參數(shù) } }; salesOffices.listen( 'squareMeter88', function( price ){ // 小明訂閱88 平方米房子的消息 console.log( '價(jià)格= ' + price ); // 輸出: 2000000 }); salesOffices.listen( 'squareMeter110', function( price ){ // 小紅訂閱110 平方米房子的消息 console.log( '價(jià)格= ' + price ); // 輸出: 3000000 }); salesOffices.trigger( 'squareMeter88', 2000000 ); // 發(fā)布88 平方米房子的價(jià)格 salesOffices.trigger( 'squareMeter110', 3000000 ); // 發(fā)布110 平方米房子的價(jià)格很明顯,現(xiàn)在訂閱者可以只訂閱自己感興趣的事件了
?
通用實(shí)現(xiàn)
有沒有辦法可以讓所有對(duì)象都擁有發(fā)布—訂閱功能呢?有的,javascript作為一門解釋執(zhí)行的語言,給對(duì)象動(dòng)態(tài)添加職責(zé)是理所當(dāng)然的事情。所以把發(fā)布—訂閱的功能提取出來,放在一個(gè)單獨(dú)的對(duì)象內(nèi):
var event = {clientList: [],listen: function( key, fn ){if ( !this.clientList[ key ] ){ this.clientList[ key ] = []; } this.clientList[ key ].push( fn ); // 訂閱的消息添加進(jìn)緩存列表 }, trigger: function(){ var key = Array.prototype.shift.call( arguments ), // (1); fns = this.clientList[ key ]; if ( !fns || fns.length === 0 ){ // 如果沒有綁定對(duì)應(yīng)的消息 return false; } for( var i = 0, fn; fn = fns[ i++ ]; ){ fn.apply( this, arguments ); // (2) // arguments 是trigger 時(shí)帶上的參數(shù) } } };再定義一個(gè)installEvent函數(shù),這個(gè)函數(shù)可以給所有的對(duì)象都動(dòng)態(tài)安裝發(fā)布—訂閱功能:
var installEvent = function( obj ){for ( var i in event ){ obj[ i ] = event[ i ]; } };下面給售樓處對(duì)象salesOffices動(dòng)態(tài)增加發(fā)布—訂閱功能
var salesOffices = {}; installEvent( salesOffices ); salesOffices.listen( 'squareMeter88', function( price ){ // 小明訂閱消息 console.log( '價(jià)格= ' + price ); }); salesOffices.listen( 'squareMeter100', function( price ){ // 小紅訂閱消息 console.log( '價(jià)格= ' + price ); }); salesOffices.trigger( 'squareMeter88', 2000000 ); // 輸出:2000000 salesOffices.trigger( 'squareMeter100', 3000000 ); // 輸出:3000000【取消訂閱】
有時(shí)候,也許需要取消訂閱事件的功能。比如小明突然不想買房子了,為了避免繼續(xù)接收到售樓處推送過來的短信,小明需要取消之前訂閱的事件。現(xiàn)在給event對(duì)象增加remove方法
event.remove = function( key, fn ){var fns = this.clientList[ key ];if ( !fns ){ // 如果key 對(duì)應(yīng)的消息沒有被人訂閱,則直接返回 return false; } if ( !fn ){ // 如果沒有傳入具體的回調(diào)函數(shù),表示需要取消key 對(duì)應(yīng)消息的所有訂閱 fns && ( fns.length = 0 ); }else{ for ( var l = fns.length - 1; l >=0; l-- ){ // 反向遍歷訂閱的回調(diào)函數(shù)列表 var _fn = fns[ l ]; if ( _fn === fn ){ fns.splice( l, 1 ); // 刪除訂閱者的回調(diào)函數(shù) } } } }; var salesOffices = {}; var installEvent = function( obj ){ for ( var i in event ){ obj[ i ] = event[ i ]; } } installEvent( salesOffices ); salesOffices.listen( 'squareMeter88', fn1 = function( price ){ // 小明訂閱消息 console.log( '價(jià)格= ' + price ); }); salesOffices.listen( 'squareMeter88', fn2 = function( price ){ // 小紅訂閱消息 console.log( '價(jià)格= ' + price ); }); salesOffices.remove( 'squareMeter88', fn1 ); // 刪除小明的訂閱 salesOffices.trigger( 'squareMeter88', 2000000 ); // 輸出:2000000?
網(wǎng)站登錄
假如正在開發(fā)一個(gè)商城網(wǎng)站,網(wǎng)站里有header頭部、nav導(dǎo)航、消息列表、購物車等模塊。這幾個(gè)模塊的渲染有一個(gè)共同的前提條件,就是必須先用ajax異步請(qǐng)求獲取用戶的登錄信息。這是很正常的,比如用戶的名字和頭像要顯示在header模塊里,而這兩個(gè)字段都來自用戶登錄后返回的信息。至于ajax請(qǐng)求什么時(shí)候能成功返回用戶信息,這點(diǎn)沒有辦法確定
但現(xiàn)在還不足以說服在此使用發(fā)布—訂閱模式,因?yàn)楫惒降膯栴}通常也可以用回調(diào)函數(shù)來解決。更重要的一點(diǎn)是,不知道除了header頭部、nav導(dǎo)航、消息列表、購物車之外,將來還有哪些模塊需要使用這些用戶信息。如果它們和用戶信息模塊產(chǎn)生了強(qiáng)耦合,比如下面這樣的形式:
login.succ(function(data){header.setAvatar( data.avatar); // 設(shè)置header 模塊的頭像nav.setAvatar( data.avatar ); // 設(shè)置導(dǎo)航模塊的頭像message.refresh(); // 刷新消息列表 cart.refresh(); // 刷新購物車列表 });現(xiàn)在必須了解header模塊里設(shè)置頭像的方法叫setAvatar、購物車模塊里刷新的方法叫refresh,這種耦合性會(huì)使程序變得僵硬,header模塊不能隨意再改變setAvatar的方法名,它自身的名字也不能被改為header1、header2。這是針對(duì)具體實(shí)現(xiàn)編程的典型例子,針對(duì)具體實(shí)現(xiàn)編程是不被贊同的
等到有一天,項(xiàng)目中又新增了一個(gè)收貨地址管理的模塊,在最后部分加上這行代碼:
login.succ(function(data){header.setAvatar( data.avatar); // 設(shè)置header 模塊的頭像nav.setAvatar( data.avatar ); // 設(shè)置導(dǎo)航模塊的頭像message.refresh(); // 刷新消息列表 cart.refresh(); // 刷新購物車列表 address.refresh(); });用發(fā)布—訂閱模式重寫之后,對(duì)用戶信息感興趣的業(yè)務(wù)模塊將自行訂閱登錄成功的消息事件。當(dāng)?shù)卿洺晒r(shí),登錄模塊只需要發(fā)布登錄成功的消息,而業(yè)務(wù)方接受到消息之后,就會(huì)開始進(jìn)行各自的業(yè)務(wù)處理,登錄模塊并不關(guān)心業(yè)務(wù)方究竟要做什么,也不想去了解它們的內(nèi)部細(xì)節(jié)。改進(jìn)后的代碼如下:
$.ajax('http://xx.com?login',function(data){ //登錄成功login.trigger('loginSucc',data); //發(fā)布登錄成功的消息 });各模塊監(jiān)聽登錄成功的消息:
var header = (function(){ // header 模塊login.listen( 'loginSucc', function( data){ header.setAvatar( data.avatar ); }); return { setAvatar: function( data ){ console.log( '設(shè)置header 模塊的頭像' ); } } })(); var nav = (function(){ // nav 模塊 login.listen( 'loginSucc', function( data ){ nav.setAvatar( data.avatar ); }); return { setAvatar: function( avatar ){ console.log( '設(shè)置nav 模塊的頭像' ); } } })();如上所述,隨時(shí)可以把setAvatar的方法名改成setTouxiang。如果有一天在登錄完成之后,又增加一個(gè)刷新收貨地址列表的行為,那么只要在收貨地址模塊里加上監(jiān)聽消息的方法即可,代碼如下:
var address = (function(){ // nav 模塊login.listen( 'loginSucc', function( obj ){ address.refresh( obj ); }); return { refresh: function( avatar ){ console.log( '刷新收貨地址列表' ); } } })();?
全局發(fā)布訂閱對(duì)象
剛剛實(shí)現(xiàn)的發(fā)布—訂閱模式,給售樓處對(duì)象和登錄對(duì)象都添加了訂閱和發(fā)布的功能,這里還存在兩個(gè)小問題:1、給每個(gè)發(fā)布者對(duì)象都添加了listen和trigger方法,以及一個(gè)緩存列表clientList,這其實(shí)是一種資源浪費(fèi);2、小明跟售樓處對(duì)象還是存在一定的耦合性,小明至少要知道售樓處對(duì)象的名字是salesOffices,才能順利的訂閱到事件
salesOffices.listen('squareMeter100',function(price){ //小明訂閱消息console.log('價(jià)格='+price); });如果小明還關(guān)心300平方米的房子,而這套房子的賣家是salesOffices2,這意味著小明要開始訂閱salesOffices2對(duì)象。見如下代碼:
salesOffices2.listen('squareMeter300',function(price){ //小明訂閱消息console.log('價(jià)格='+price); });其實(shí)在現(xiàn)實(shí)中,買房子未必要親自去售樓處,只要把訂閱的請(qǐng)求交給中介公司,而各大房產(chǎn)公司也只需要通過中介公司來發(fā)布房子信息。這樣一來,不用關(guān)心消息是來自哪個(gè)房產(chǎn)公司,在意的是能否順利收到消息。當(dāng)然,為了保證訂閱者和發(fā)布者能順利通信,訂閱者和發(fā)布者都必須知道這個(gè)中介公司
同樣在程序中,發(fā)布—訂閱模式可以用一個(gè)全局的Event對(duì)象來實(shí)現(xiàn),訂閱者不需要了解消息來自哪個(gè)發(fā)布者,發(fā)布者也不知道消息會(huì)推送給哪些訂閱者,Event作為一個(gè)類似“中介者”的角色,把訂閱者和發(fā)布者聯(lián)系起來。見如下代碼:
var Event = (function(){var clientList = {},listen,trigger,remove;listen = function( key, fn ){if ( !clientList[ key ] ){ clientList[ key ] = []; } clientList[ key ].push( fn ); }; trigger = function(){ var key = Array.prototype.shift.call( arguments ), fns = clientList[ key ]; if ( !fns || fns.length === 0 ){ return false; } for( var i = 0, fn; fn = fns[ i++ ]; ){ fn.apply( this, arguments ); } }; remove = function( key, fn ){ var fns = clientList[ key ]; if ( !fns ){ return false; } if ( !fn ){ fns && ( fns.length = 0 ); }else{ for ( var l = fns.length - 1; l >=0; l-- ){ var _fn = fns[ l ]; if ( _fn === fn ){ fns.splice( l, 1 ); } } } }; return { listen: listen, trigger: trigger, remove: remove } })(); Event.listen( 'squareMeter88', function( price ){ // 小紅訂閱消息 console.log( '價(jià)格= ' + price ); // 輸出:'價(jià)格=2000000' }); Event.trigger( 'squareMeter88', 2000000 ); // 售樓處發(fā)布消息【模塊間通信】
上面實(shí)現(xiàn)的發(fā)布—訂閱模式的實(shí)現(xiàn),是基于一個(gè)全局的Event對(duì)象,利用它可以在兩個(gè)封裝良好的模塊中進(jìn)行通信,這兩個(gè)模塊可以完全不知道對(duì)方的存在
比如現(xiàn)在有兩個(gè)模塊,a模塊里面有一個(gè)按鈕,每次點(diǎn)擊按鈕之后,b模塊里的div中會(huì)顯示按鈕的總點(diǎn)擊次數(shù),用全局發(fā)布—訂閱模式完成下面的代碼,使得a模塊和b模塊可以在保持封裝性的前提下進(jìn)行通信
<button id="count">點(diǎn)我</button> <div id="show"></div> <script type="text/JavaScript"> var a = (function(){ var count = 0; var button = document.getElementById( 'count' ); button.onclick = function(){ Event.trigger( 'add', count++ ); } })(); var b = (function(){ var div = document.getElementById( 'show' ); Event.listen( 'add', function( count ){ div.innerHTML = count; }); })(); </script>但要留意一個(gè)問題,模塊之間如果用了太多的全局發(fā)布—訂閱模式來通信,那么模塊與模塊之間的聯(lián)系就被隱藏到了背后。最終會(huì)搞不清楚消息來自哪個(gè)模塊,或者消息會(huì)流向哪些模塊,這又會(huì)給維護(hù)帶來一些麻煩,也許某個(gè)模塊的作用就是暴露一些接口給其他模塊調(diào)用
【先發(fā)布后訂閱】
常見的發(fā)布—訂閱模式,都是訂閱者必須先訂閱一個(gè)消息,隨后才能接收到發(fā)布者發(fā)布的消息。在某些情況下,需要先將這條消息保存下來,等到有對(duì)象來訂閱它的時(shí)候,再重新把消息發(fā)布給訂閱者。就如同QQ中的離線消息一樣,離線消息被保存在服務(wù)器中,接收人下次登錄上線之后,可以重新收到這條消息
/**************先發(fā)布后訂閱********************/ Event.trigger('click',1); Event.listen('click',function(a){ console.log(a); //輸出:1 });這種需求在實(shí)際項(xiàng)目中是存在的,比如在商城網(wǎng)站中,獲取到用戶信息之后才能渲染用戶導(dǎo)航模塊,而獲取用戶信息的操作是一個(gè)ajax異步請(qǐng)求。當(dāng)ajax請(qǐng)求成功返回之后會(huì)發(fā)布一個(gè)事件,在此之前訂閱了此事件的用戶導(dǎo)航模塊可以接收到這些用戶信息
但是這只是理想的狀況,因?yàn)楫惒降脑?#xff0c;不能保證ajax請(qǐng)求返回的時(shí)間,有時(shí)候它返回得比較快,而此時(shí)用戶導(dǎo)航模塊的代碼還沒有加載好(還沒有訂閱相應(yīng)事件),特別是在用了一些模塊化惰性加載的技術(shù)后,這是很可能發(fā)生的事情。也許還需要一個(gè)方案,使得的發(fā)布—訂閱對(duì)象擁有先發(fā)布后訂閱的能力
為了滿足這個(gè)需求,要建立一個(gè)存放離線事件的堆棧,當(dāng)事件發(fā)布的時(shí)候,如果此時(shí)還沒有訂閱者來訂閱這個(gè)事件,暫時(shí)把發(fā)布事件的動(dòng)作包裹在一個(gè)函數(shù)里,這些包裝函數(shù)將被存入堆棧中,等到終于有對(duì)象來訂閱此事件的時(shí)候,將遍歷堆棧并且依次執(zhí)行這些包裝函數(shù),也就是重新發(fā)布里面的事件。當(dāng)然離線事件的生命周期只有一次,就像QQ的未讀消息只會(huì)被重新閱讀一次,所以剛才的操作只能進(jìn)行一次
【全局事件的命名沖突】
全局的發(fā)布—訂閱對(duì)象里只有一個(gè)clinetList來存放消息名和回調(diào)函數(shù),大家都通過它來訂閱和發(fā)布各種消息,久而久之,難免會(huì)出現(xiàn)事件名沖突的情況,所以還可以給Event對(duì)象提供創(chuàng)建命名空間的功能
/**************使用命名空間********************/ Event.create('namespace1').listen('click',function(a){ console.log(a); //輸出:1 }); Event.create('namespace1').trigger('click',1); Event.create('namespace2').listen('click',function(a){ console.log(a); //輸出:2 }); Event.create('namespace2').trigger('click',2);下面是完整代碼
var Event = (function(){var global = this, Event, _default = 'default'; Event = function(){ var _listen, _trigger, _remove, _slice = Array.prototype.slice, _shift = Array.prototype.shift, _unshift = Array.prototype.unshift, namespaceCache = {}, _create, find, each = function( ary, fn ){ var ret; for ( var i = 0, l = ary.length; i < l; i++ ){ var n = ary[i]; ret = fn.call( n, i, n); } return ret; }; _listen = function( key, fn, cache ){ if ( !cache[ key ] ){ cache[ key ] = []; } cache[key].push( fn ); }; _remove = function( key, cache ,fn){ if ( cache[ key ] ){ if( fn ){ for( var i = cache[ key ].length; i >= 0; i-- ){ if( cache[ key ] === fn ){ cache[ key ].splice( i, 1 ); } } }else{ cache[ key ] = []; } } }; _trigger = function(){ var cache = _shift.call(arguments), key = _shift.call(arguments), args = arguments, _self = this, ret, stack = cache[ key ]; if ( !stack || !stack.length ){ return; } return each( stack, function(){ return this.apply( _self, args ); }); }; _create = function( namespace ){ var namespace = namespace || _default; var cache = {}, offlineStack = [], // 離線事件 ret = { listen: function( key, fn, last ){ _listen( key, fn, cache ); if ( offlineStack === null ){ return; } if ( last === 'last' ){ }else{ each( offlineStack, function(){ this(); }); } offlineStack = null; }, one: function( key, fn, last ){ _remove( key, cache ); this.listen( key, fn ,last ); }, remove: function( key, fn ){ _remove( key, cache ,fn); }, trigger: function(){ var fn, args, _self = this; _unshift.call( arguments, cache ); args = arguments; fn = function(){ return _trigger.apply( _self, args ); }; if ( offlineStack ){ return offlineStack.push( fn ); } return fn(); } }; return namespace ? ( namespaceCache[ namespace ] ? namespaceCache[ namespace ] : namespaceCache[ namespace ] = ret ) : ret; }; return { create: _create, one: function( key,fn, last ){ var event = this.create( ); event.one( key,fn,last ); }, remove: function( key,fn ){ var event = this.create( ); event.remove( key,fn ); }, listen: function( key, fn, last ){ var event = this.create( ); event.listen( key, fn, last ); }, trigger: function(){ var event = this.create( ); event.trigger.apply( this, arguments ); } }; }(); return Event; })();發(fā)布—訂閱模式,也就是常說的觀察者模式,它的優(yōu)點(diǎn)非常明顯,一為時(shí)間上的解耦,二為對(duì)象之間的解耦。應(yīng)用也非常廣泛,既可以用在異步編程中,也可以幫助完成更松耦合的代碼編寫。發(fā)布—訂閱模式還可以用來幫助實(shí)現(xiàn)一些別的設(shè)計(jì)模式,比如中介者模式。從架構(gòu)上來看,無論是MVC還是MVVM,都少不了發(fā)布—訂閱模式的參與,而且javascript本身也是一門基于事件驅(qū)動(dòng)的語言
當(dāng)然,發(fā)布—訂閱模式也不是完全沒有缺點(diǎn)。創(chuàng)建訂閱者本身要消耗一定的時(shí)間和內(nèi)存,而且訂閱一個(gè)消息后,也許此消息最后都未發(fā)生,但這個(gè)訂閱者會(huì)始終存在于內(nèi)存中。另外,發(fā)布—訂閱模式雖然可以弱化對(duì)象之間的聯(lián)系,但如果過度使用的話,對(duì)象和對(duì)象之間的必要聯(lián)系也將被深埋在背后,會(huì)導(dǎo)致程序難以跟蹤維護(hù)和理解。特別是有多個(gè)發(fā)布者和訂閱者嵌套到一起的時(shí)候,要跟蹤一個(gè)bug不是件輕松的事情。
?
需要購買阿里云產(chǎn)品的,可以點(diǎn)擊此鏈接購買,有紅包優(yōu)惠哦! https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=fp9ccf07
轉(zhuǎn)載于:https://www.cnblogs.com/libo0125ok/p/8038073.html
總結(jié)
以上是生活随笔為你收集整理的javascript-发布订阅模式的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 鲁迅经典名言名句198个
- 下一篇: JS容易犯错的this和作用域