一、什麼是發佈訂閱模式?ajax
發佈訂閱模式,在咱們生活中是很是常見的一種,好比咱們常見的微信公衆號訂閱號,被訂閱的公衆號做者會將更新的文章發送給每一個訂閱者,再好比咱們找中介買房子,告訴了中介咱們的需求(訂閱),而後中介手上有了適合的房源後,將信息發送給全部訂閱的人(發佈)等。緩存
二、看一個最簡單發佈訂閱的代碼例子微信
let e = { arr:[], on(fn){ this.arr.push(fn); }, emit(){ this.arr.forEach(fn=>fn()); } } e.on(()=>{ console.log("haha") }) e.on(()=>{ console.log("是否是傻") }) function sendMsg(){ e.emit() } sendMsg()
咱們定義了一個對象e,它有兩個方法,on:用於接收訂閱的事件,將事件存儲在變量arr中,emit:用於遍歷全部訂閱的事件,並執行app
而後在調用方法sendMsg()的時候,執行e.emit()方法。異步
還有前面咱們使用after函數讀取文件的例子,同時去讀取文件,當兩個文件讀取完成以後,須要打印出兩個文件的內容,這個也可使用咱們的發佈訂閱來完成。代碼以下:ide
const e = { arr:[], on(fn){ this.arr.push(fn); }, emit(){ this.arr.forEach(fn=>fn()); } } let info = {}; e.on(()=>{console.log("我讀完了")}) e.on(()=>{ if(Object.keys(info).length==2){ console.log(info); } }); fs = require("fs"); fs.readFile("name.txt","utf8",(err,data)=>{ info["name"]=data; e.emit(); }) fs.readFile("age.txt","utf8",(err,data)=>{ info["age"]=data; e.emit(); })
上面這個例子中,我先訂閱了兩個方法,一個函數做用是輸出我讀完了,一個函數用於判斷當前存儲文件內容的對象是否有兩項,有的話,說明已經讀完了。而後讓fs分別去讀取各自的文件name.txt和age.txt,每次讀取完成都調用發佈函數e.emit(),而後去輪流執行前面訂閱的兩個方法函數
三、發佈訂閱模式的優勢ui
1)發佈訂閱模式最大的優勢就是,解耦。this
發佈訂閱模式能夠取代對象之間的硬編碼的通知機制。一個對象不用再顯示的調用另外一個對象的某個接口。且當有新的訂閱者出現時,發佈者不用修改任何代碼,一樣發佈者發生改變時,以前的訂閱者也不會受到影響。編碼
舉一個生活中的實例,好比我和朋友一塊兒開了一個店,僱傭了一個店員幫咱們看店,而後咱們想要了解店裏的經營狀況,因此但願店員每成交一個訂單就給咱們分別發一條短信通知。
要實現這個功能,咱們須要分三步走:
a)須要定義一個發佈者,如例子中的店員
b)發佈者須要一個緩存列表,用於存放回調函數,以便通知訂閱者
c)發佈者發佈消息的時候,須要遍歷緩存列表,依次觸發訂閱函數
const employee = { employer:[], // 用於存放我和朋友的回調函數 listen(fn){ this.employer.push(fn) }, trigger(...args){ this.employer.forEach(fn=>fn(...args)) } } employee.listen((salesAmount)=>{ console.log("我:",salesAmount); // 我:800 }) employee.listen((salesAmount)=>{ console.log("朋友: ", salesAmount); // 朋友:800 }) employee.trigger(800);
上面就實現了一個簡單的發佈訂閱,可是過了一段時間後,店裏生意變得愈來愈好,天天的訂單愈來愈多,我和朋友天天會收到不少不少短信,因此但願將模式修改一下,我和朋友決定分工合做,我收取全部訂單金額大於200的訂單信息,朋友接受全部訂單金額小於200的訂單信息。要實現這個功能,咱們須要在訂閱的時候,傳遞一個參數key,說明咱們的需求,而後店員在發佈的時候,也傳遞對應的key進行區分,代碼以下:
const employee = { employer:[], // 存放訂閱信息的緩存列表 listen(key, fn){ // 訂閱消息 if(this.employer.indexOf(key)==-1){ this.employer[key] = []; // 若是尚未訂閱過此類信息,給此類信息建立一個緩存列表 } this.employer[key].push(fn); // 訂閱的消息,添加到緩存列表中 }, trigger(){ // 發佈消息 let key = Array.prototype.shift.call(arguments); // 取出消息類型 lt200;gt200 let fns = this.employer[key] // 取出對應的方法 if(!fns || fns.length==0){ return; } fns.forEach(fn=>{ fn(arguments); // 執行訂閱方法(這裏的arguments是已經去掉了消息類型的剩餘參數) }) } } employee.listen("lt200",(salesAmount)=>{ // 訂閱 console.log("朋友: ",salesAmount); }) employee.listen("gt200",(salesAmount)=>{ console.log("我:",salesAmount); }) employee.trigger("lt200", 80); // 發佈 employee.trigger("gt200",600);
這樣,我和朋友就能夠按照本身的須要分別收到信息了。可是有一天朋友以爲,每天收那麼多信息,太麻煩了,不想再收到信息了,怎麼辦呢?聰明的你確定想到了,既然有訂閱,就應該有取消訂閱啊,因此接下來,咱們須要給對象添加一個取消訂閱的方法
employee.cancelListen = function(key, fn){ // 取消訂閱 let func = this.employer[key]; if(!func){ // 說明對應的key沒有人訂閱 return false; } if(!fn){ // 取消訂閱的人沒有傳遞須要取消哪一個訂閱,則將當前key下對應的全部訂閱所有取消 fn.length = 0; }
for(let l=func.length-1;i>=0;l--){
if(fn==func[l]){
func.splice(l,1)
}
} }
function a(...args){
console.log("我",...args)
}
employee.listen("gt200",a)
console.log(employee.trigger("gt200",900)) // 我900
employee.cancelListen("gt200",a)
console.log(employee.trigger("gt200", 900)) // 此時沒有輸出,由於訂閱已經被取消了
可是咱們的程序裏面常常會有這種狀況,因爲ajax異步,可能在程序尚未訂閱的時候,發佈程序已經執行了,因此咱們須要先將發佈事件暫存起來,等到有人來訂閱的時候,再執行。
const event = { // 訂閱 cache:{}, stackFn:{}, listen(key, fn){ if(!cache[key]){ cache[key] = []; } cache[key].push(fn); let stackArgs_ = stackFn[key]; if(!stackArgs_){ return; } stackArgs.forEach(args,=>{ fn(...args); }) }, trigger(){ let key = Array.prototype.shift.call(arguments);// 取出第一個參數 let fns = this.cache[key]; if(!fns || fns.length==0){ if(!this.stackFn[key]){ this.stackFn[key] = []; } this.stackFn[key].push(arguments); return; } fns.forEach(fn_=>{ fn_(...arguments) }) } }
完整代碼以下:
1 const event = { 2 let Event, _default = "default"; 3 Event = function(){ 4 let each, _create, _remove, _listen, _trigger, namespaceCache, _shift=Array.prototype.shift; 5 each = function(ary, fn){ 6 ary.forEach(item=>{ 7 fn.call(item); 8 }); 9 } 10 _create = function(key, fn, cache){ 11 // 訂閱 12 if(!cache[key]){ 13 cache[key] = []; 14 } 15 cache[key].push(fn); 16 } 17 18 _trigger = function(){ 19 let key = _shift(arguments); 20 let args = _shift(arguments); 21 let cache = _shift(arguments); 22 let self = this; 23 fns = cache[key]; 24 if(!fns || fns.length==0){ 25 return; 26 } 27 each(fns, function(){ 28 this.apply(self, args); 29 }) // 至關於 fns.forEach(fn=>{fn(...args)}) 30 31 _remove = function(key, fn, cache){ 32 // 取消訂閱 33 let fns = cache[key]; 34 if(!fns || fns.length==0){ 35 return; 36 } 37 if(!fn){ 38 fns = []; 39 } 40 for(let i=0;i<fns.length;i++){ 41 if(fns[i]==fn){ 42 fns.shift(i,1); 43 } 44 } 45 } 46 _create = function(namespace){ 47 // 建立訂閱 48 namespace = namespace||_default; 49 let ret, cache={}, offlineCache={}; 50 ret = { 51 listen:(key, fn)=>{ 52 // 建立訂閱:1)調用event的訂閱方法,執行訂閱操做;2)因爲程序的異步,可能會出現執行發佈的時候,訂閱還沒有完成,因此須要在訂閱的時候,查看是否有離線信息,若是有的話,執行離線信息 53 _listen(key, fn, cache); 54 offlineFn = offlineCache[key]; 55 if(!offlineFn){ 56 return 57 } 58 each(offlineFn, function(){ 59 this(); 60 }) // 至關於offlineFn.forEach(fn=>{fn(key, fn, cache)}) 61 offlineCache[key]= [];// 離線信息執行一次以後,就清除 62 }, 63 remove:(key, fn)=>{ 64 // 取消訂閱,直接調用event的取消訂閱便可 65 _remove(key, fn, cache); 66 }, 67 trigger:(key, ...args)=>{ 68 // 發佈,調用event的_trigger 69 _trigger(key, args, cache); 70 // 判斷是否有訂閱,若是沒有訂閱,須要將它加入離線緩存中 71 let stack = cache[key] 72 if(stack && stack.length>0){ 73 return; 74 } 75 let fn = function(){ 76 return _trigger.apply(self, [key, args, cache]); 77 } 78 if(offlineCache && offlineCache[key].length==0){ 79 offlineCache[key] = []; 80 } 81 offlineCache[key].push(fn); 82 } 83 } 84 return namespace?(namespaceCache[namespace]?namespaceCache[namespace]:namespaceCache[namespace]=ret):ret; 85 } 86 return { 87 create:_create, 88 listen:function(obj, fn){ 89 let event = this.create(obj['namespace']); 90 event.listen(obj["key"], fn); 91 }, 92 remove:function(obj, fn){ 93 let event = this.create(obj['namespace']); 94 event.remove(obj['key'], fn); 95 } 96 trigger:function(obj, ...args){ 97 let event = this.create(obj["namespace"]); 98 event.trigger.apply(this, obj[key], ...args); 99 } 100 } 101 }(); 102 return Event; 103 })();