前言: 從八月份入職以來,在可以保證專案進度後,我便開始思考,怎麼把事情做得更好,怎麼提升自己。
- 一方面,提升自己對javascript這門語言的理解,我在udemy上買了 JavaScript: Understanding the Weird Parts.中文翻譯過來,就是javascript: 理解怪異的部分。很經典,我推薦每個越過了基礎這道坎的人去看一下這部分內容。我也買了書,之後計劃對每一章進行解讀。
- 另一方面,我明白了js是一門程式語言,是工具。那麼工具的用法是有很多種的。在不同的場景,使用不同的方法去處理,會讓你開發速度事半功倍。也可以提升自己對問題不同的解決方案。所以我閱讀了《javascript設計模式與開發實踐》,想知道更好的組織程式碼的形式是怎樣,在同一場景下,別人是怎麼處理問題的。
- 對於個人提升方面,可以單獨拿一篇來探討了。鑑於篇幅,只說兩點。
正文開始
什麼是釋出訂閱模式
釋出訂閱模式稱觀察者模式,它定義物件間的一種一對多的依賴關係,當一個物件狀態改變的時候,所有依賴於它物件都將得到通知.
有點繞哈。其實說得簡單一點。你關注了我,我更新了文章,你會得到推送,就這個意思。 其實在日常的開發中,你一直在使用著釋出訂閱模式進行開發。最常見的例子是,原生事件API。(也就是滑鼠點選/移動/進入等事件,就是使用了釋出訂閱模式)
來,舉個栗子
// 訂閱
document.body.addEventListener('click', function() {
alert(2);
});
// 觸發事件釋出
document.body.click();
複製程式碼
在釋出訂閱模式中,有兩個物件,一個是事件的釋出者,一個是訂閱者。
好啦,回答我一個問題,然後繼續看下去:
- 在例子中,誰是釋出者?
- 在例子中,誰是訂閱者?
假設你答出來了,OK,那麼接下來很容易理解。如果沒有,那沒關係,先看答案: * 釋出者是document.body * 訂閱者是我們 我們訂閱了在document.body上的click事件,當使用者點選了body,那麼會觸發click事件,body節點向使用者也就是我傳送資訊(alert). 使用這個模式還有個優點是:
我們可以隨意的增加或者刪除事件,這對訂閱者不會產生任何影響。
實現釋出訂閱模式
在我們理解了釋出者和訂閱者的關係後,來完成一個官方例項: 假設,現在有一個售樓處, 售樓處作為釋出者,而買家作為訂閱者。當價格變動的時候,售樓處把價格資訊推送給訂閱者。
// 實現一個釋出訂閱的步驟
- 指定好釋出的物件是誰?
- 給釋出者一個快取佇列,存放回撥函式以便通知訂閱者。
- 釋出訊息遍歷這個快取佇列,以此觸發裡面存放的訂閱者回撥函式。(符合條件的就進行觸發)
第一版:
var selfOffices = {} // 定義釋出者
selfOffices.clientList = [] // 快取佇列,用來存放回撥函式
// 增加訂閱
selfOffices.listen = function (fn) {
this.clientList.push(fn)
}
// 觸發事件釋出
selfOffices.trigger = function () {
for (let i = 0, fn; fn = this.clientList[i++];) {
fn.apply(this, arguments);
}
}
//訂閱例項
selfOffices.listen(function (price, squareMeter) {
console.log('價格 = ', price)
console.log('squareMeter = ', squareMeter)
})
//訂閱例項
selfOffices.listen(function (price, squareMeter) {
console.log('價格 = ', price)
console.log('squareMeter = ', squareMeter)
})
//觸發
selfOffices.trigger(2000000, 90)
selfOffices.trigger(21321312321, 100)
複製程式碼
至此實現了最基本的釋出訂閱模式,但是你發現問題了嗎?
- 當我觸發其中一個訂閱的時候,在上面的模式下,釋出者把其他使用者的訂閱也釋出給了我。
解決方案是增加一個標識。(就像onclick, onmousemove, 你訂閱click事件,在mousemove事件觸發時,你不會接收到通知)
第二版:
var selfOffices = {}
selfOffices.clientList = []
// 重要: 在這裡,增加了key關鍵字,作為標識位
selfOffices.listen = function (key, fn) {
if (!this.clientList[key]) {
this.clientList[key] = []
}
this.clientList[key].push(fn)
}
selfOffices.trigger = function () {
// 重要:在觸發之前進行一個判斷,如果在觸發的事件該訂閱者沒有訂閱,則不會執行相應的訂閱事件
var key = Array.prototype.shift.call(arguments)
fns = this.clientList[key]
if (!fns || fns.length == 0) {
return false
}
for (let i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments);
}
}
selfOffices.listen("square88", function (price) {
console.log('價格 = ', price)
})
selfOffices.listen("square100", function (price) {
console.log('價格 = ', price)
})
selfOffices.trigger('square88', 90)
selfOffices.trigger('square100', 100)
複製程式碼
至此,完成了釋出特定訊息,訂閱者訂閱的事件釋出的時候通知訂閱了特定訊息的人。
第三版: 讓我們把以上的流程抽象出來,變成一個通用的釋出訂閱模式
// 釋出訂閱模式的通用模式
// 釋出者
var event = {
clientList: [],// 監聽佇列
listen: function (key, fn) {// 訂閱
if (!this.clientList[key]) {
this.clientList[key] = []
}
this.clientList[key].push(fn)
},
trigger: function () {// 觸發
var key = Array.prototype.shift.call(arguments),
fns = this.clientList[key];
if (!fns || fns.length == 0) {
return false
}
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments)
}
}
}
// 訂閱者
var installEvent = function (obj) {
for (var i in event) {
obj[i] = event[i]
}
}
// 測試
var sales = {}// 訂閱者
installEvent(sales)// 初始化訂閱者
sales.listen('88', function (price) {
console.log(88)
})
sales.listen('99', function (price) {
console.log(99)
})
sales.trigger('88')
sales.trigger("99")
複製程式碼
至此, 完成了一個非破壞性的通用釋出訂閱模式。
第四版: 你知道的,可以訂閱,就一定要有取消訂閱的功能,不然。。。你看addEventListener.很尷尬。(無法取消)
(這裡偷懶,把第三版的程式碼假裝放在這裡)
// but, 訂閱完成之後,我突然的又不想再繼續訂閱這個事件了,因為我找到更加好的了
// 為我們的釋出訂閱函式增加取消訂閱的功能
event.remove = function (key, fn) {
// 根據key在快取找到對應的快取佇列
var fns = this.clientList[key]
if (!fns) {
return false
}
// 如果沒有傳入fn那麼,清空該條快取佇列
if (!fn) {
fns && (fns.length == 0)
} else {
// 相反,如果存在fn,那麼遍歷快取佇列,刪除該條快取佇列中的事件
for (var l = fns.length - 1; l >= 0; l--) {
var _fn = fns[l]
if (_fn === fn) {
fns.splice(l, 1)
}
}
}
}
複製程式碼
雖然,這已經很棒了XD,但是,還是存在一定的問題,問題體現在以下幾個方面:
- 我們在給每一個物件新增listen和trigger方法,以及一個clientList列表,其實沒有這個必要
- 訂閱者與釋出者之間還是存在一定的耦合關係,如果訂閱者不知道釋出者的名稱,那就無法進行訂閱,
- 又或者,訂閱者想訂閱另一個釋出者的事件,那麼還是要去獲取到另一個釋出者的名稱才能訂閱到
解決方案: 使用全域性Event物件實現,訂閱者不需要知道訊息來自哪裡,釋出者了也不知道資訊要釋出給誰 Event物件作為中介,連結兩者(訂閱者,釋出者)。
第五版: 用立即執行函式,形成閉包。對外暴露出Event介面。供外界使用。
var Event = (function () {
var clientList = [],
listen,
trigger,
remove;
listen = function (key, fn) {
if (!clientList[key]) {
clientList[key] = []
}
clientList[key].push(fn)
}
trigger = function (key) {
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( '99', function (price) {
console.log(price);
})
Event.trigger('99', 2999)
// Event.remove('99')
// Event.trigger('99', 299)
複製程式碼
額,第五版,哎呀,寫得太棒了,感覺沒啥問題了。但是想想,在大型應用中,使用釋出訂閱模式很可能,很可能很多個。那麼在以上的模式下,到最後,clientList會有些膨脹。可能造成很多很多的事件集中在這裡。不好管理,以及debugger. 所以,我們迎來了第六版!為釋出訂閱模式提供名稱空間的能力!更好的管理每個事件,可以對每類事件分門別類的放好。安排!
第六版: 對第五版的程式碼進行增強,提供名稱空間的能力 第六版,其實看起來有點多,其實就是增加了一個create還好還好,如果覺得比較困難你可以收藏,未來再回來看會好很多。
// todo 為了使釋出訂閱模式更加適用。我們要對上個版本的釋出訂閱模式進行增強。提供名稱空間的能力。更好的管理每個釋出訂閱。
var Event = (function () {
// 相容各個平臺,因為broswer的global是window, 而node.js的是global
var global = this,
Event,// 初始化掛載點
_default = 'default';// 初始化名稱空間
Event = function () {
// 初始化Event各個方法:監聽,觸發,移除
var _listen,
_trigger,
_remove,
// 初始化工具方法
_slice = Array.prototype.slice,
_shift = Array.prototype.shift,
_unshift = Array.prototype.unshift,
// 初始化名稱空間快取
namespaceCache = {},
// 初始化以名稱空間作為event的方法
_create,
// ! 這個find就很迷了,不知道什麼作用, 求各位大佬解答
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;
};
// 監聽: 如果這個監聽的名稱在監聽快取中不存在, 那麼,初始化, 並且把該監聽事件存入cache[key]陣列中。
_listen = function(key, fn, cache) {
if( !cache [key]){
cache[key] = []
}
cache[key].push(fn);
};
// 移除: 首先判斷監聽快取佇列中是否存在對應的記錄, 如果存在,在對應的cache[key]陣列中刪除對應的監聽事件。
_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] = [];
}
}
};
// 觸發: 取出cache佇列, 迭代佇列,觸發事件
_trigger = function () {
var cache = _shift.call(arguments),// 取出cache佇列
key = _shift.call(arguments),// 取出對應的key, 像“click”
args = arguments,// 經過以上兩步, 剩下的只有入參了
_self = this,// 在這一步,獲取this,也就是Event物件本身
ret,
// 獲得觸發棧, 也就是之前使用listen設定的監聽事件
stack = cache[key];
if(!stack || !stack.length ) {
return;
}
return each(stack, function (){
// 此時this指向stack中每個匿名函式
return this.apply(_self, args);
});
};
// 建立名稱空間的方法
_create = function (namespace) {
// 給名稱空間設定預設值
var namespace = namespace || _default;
// 初始化cache和離線棧
var cache = {},
offlineStack = [],
// 這個ret最後會掛載到名稱空間(namespaceCache)的快取中
ret = {
listen: function (key, fn, last) {
_listen(key, fn, cache);
if(offlineStack == null) {
return;
}
if(last == 'last') {
offlineStack.length && offlineStack.pop()();
}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 namespaceCache ? (namespaceCache[namespace] ? namespaceCache [namespace] : namespaceCache[namespace] = ret) :ret;
};
return {
// 使用全域性Event時的返回
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;
}())
// 使用範例
// 先發布後訂閱
Event.trigger('click', 1);
Event.listen('click', function (a) {
console.log(a)
});
// 使用名稱空間,讓各個訂閱事件整潔有序
Event.create('namespace1').listen('click', function (a) {
console.log(a)
});
Event.create('namespace1').trigger('click', 1);
Event.create('namespace2').listen('click', function (a) {
console.log(a)
});
Event.create('namespace2').trigger('click', 1);
複製程式碼
(完)
OK,不知道大家感覺怎麼樣。如果你看到了這裡。謝謝你。我認為自己做的事情有價值,能給大家帶來幫助就會讓我很有成就感。
稍微橫向擴充套件一下,釋出訂閱模式在js這門語言中用在很多地方: node.js的事件驅動模型以及vue中的自定義事件,在我看來,都使用了釋出訂閱這種思想
篇幅有限,週末還有半天,以上兩點就不繼續寫下去了。
另外,掘金社群的各位大佬,感謝批評指正。希望得到一些正向,中肯的評價。感謝各位大佬。
參考: 《javascript設計模式與開發實踐》