觀察者模式(JS描述)
觀察者模式又名"釋出-訂閱"模式,能夠有效的將模組間進行解耦。
觀察者模式是運用在一些有一對多關係的場景中,一個物件有了一些改變,其他依賴於該物件的物件也要做一些動作。
比如,上課,老師提了一個問題,所有會這道問題的學生都解答這個問題。其中老師就是主物件,老師提問就是狀態改變,學生是依賴於老師的物件,學生回答問題就是針對老師提問這個狀態所做的改變。
弄清楚這個原理,我們來看看怎麼用程式碼實現。
我們可以建立一個物件幫我們監控主物件得改變,而且要通知訂閱了主物件改變這個時間的依賴物件做出相應,還以上面老師提問學生答為例,我們創一個Observer的物件,它得能幫學生去訂閱老師提問某個問題,也得能幫老師發出提問得訊息,而且既然有訂閱,就得有解除訂閱,所以得有一個解除訂閱得方法,基於這個思路,我們設計的Observer差不多就是以下這個樣子:
const Observer = (function(){
var _messages = {};
return {
add: ()=>{}, //訂閱訊息
emit: ()=>{}, //釋出訊息
remove: ()=>{} //取消訊息的訂閱
}
})()
用立即執行函式建立一個私有變數,_messages存放被訂閱的訊息,以及訂閱者所要執行的操作。
接下來我們看add方法,add要把被訂閱的訊息,以及訂閱者的操作存放到 _messages 裡,以供在訊息發出時,訂閱者能夠做出相應迴應。所以add方法可以這麼實現:
add: (type, fn)=>{ //type就是訊息,fn就是訂閱者的回撥
if(_messages[type]){
_messages[type].push(fn)
}else{
_messages[type] = [fn]
}
}
方法很簡單,就是看_messages裡有沒有某個訊息,如果沒有就建立陣列,把回撥方里,否者直接放數組裡。因為可能有多個訂閱者訂閱同一個訊息,所以要用陣列。
接下來是emit方法,emit用於發出某個訊息,其實就是去_messages中看某個訊息有沒有訂閱者,有的話就把所有的回撥執行一遍。
emit: (type, args)=>{
if(_messages[type] && _messages[type] instanceof Array){
for(let i = 0; i < _messages[type].length; i++){
typeof _messages[type][i] == 'function' && _messages[type][i].call(this, args)
}
}
}
最後就是remove方法,其實就是看某個訊息找某個訂閱者的回撥,刪掉就行。
remove: (type, fn)=>{
if(_messages[type] && _messages[type] instanceof Array){
for(let i = _messages[type].length - 1; i >= 0; i--){
_messages[type][i] == fn && _messages[type].splice(i, 1);
}
}
}
組合起來,一個完整的觀察者物件就是下面這樣:
const Observer = (function(){
var _messages = {};
return {
add: (type, fn)=>{
if(_messages[type]){
_messages[type].push(fn)
}else{
_messages[type] = [fn]
}
},
emit: (type, args)=>{
if(_messages[type] && _messages[type] instanceof Array){
for(let i = 0; i < _messages[type].length; i++){
typeof _messages[type][i] == 'function' && _messages[type][i].call(this, args)
}
}
},
remove: (type, fn)=>{
if(_messages[type] && _messages[type] instanceof Array){
for(let i = _messages[type].length - 1; i >= 0; i--){
_messages[type][i] == fn && _messages[type].splice(i, 1);
}
}
}
}
})()
接下來,讓我們看看它是怎麼工作的,還以學生答問題為例,我們先來一個老師,老師就是釋出提問:
var Teacher = function(){
this.question = function(questionName){
console.log('老師提問了: ' + questionName);
Observer.emit(questionName, {question: questionName})
}
}
然後是學生,學生有一個回答問題的方法,有一個監聽問題的方法,只監聽自己會的問題,另外還有一個睡覺方法,睡覺其實就是取消訂閱某個訊息。
var Student = function(name){
this.name = name;
this.answer = function(args){
console.log(name +'回答老師的問題: ' + args.question);
}
}
Student.prototype = {
listen: function(questionName){
console.log(this.name +'想要回答問題: ' + questionName);
Observer.add(questionName, this.answer)
},
sleep: function(questionName){
console.log(this.name +'睡著了');
Observer.remove(questionName, this.answer)
}
}
listen和sleep可以放到原型鏈上,但是answer方法放在例項上了,因為如果再放在原型鏈上,觀察者就沒法分清哪個回撥是哪個訂閱者的了。
有了以上兩個物件,我們結合下面的程式碼,看看觀察者怎麼工作:
var s1 = new Student('小明');
var s2 = new Student('小紅');
s1.listen("誰最帥");
s2.listen("誰最帥");
var t = new Teacher();
setTimeout(() => {
t.question('誰最帥');
s1.sleep("誰最帥");
setTimeout(() => {
t.question('誰最帥')
}, 2000);
}, 2000);
我們建立兩個學生,分別監聽了'誰最帥'這個問題,然後建立一個老師,老師兩秒後提問'誰最帥',兩個學生應該都回答該問題。然後學生s1睡著了,於是不再監聽'誰最帥',老師2秒後又提問,這次只有學生s2回答該問題。
完整的結果是下面這樣的。
小明想要回答問題: 誰最帥
小紅想要回答問題: 誰最帥
老師提問了: 誰最帥
小明回答老師的問題: 誰最帥
小紅回答老師的問題: 誰最帥
小明睡著了
老師提問了: 誰最帥
小紅回答老師的問題: 誰最帥
會出現上面的結果就是觀察者在起作用,兩個學生監聽提問,Observer往_messages中插入了'誰最帥',而且把兩位學生的回答方法放進了數組裡,等老師提問,Observer就去_messages中依次執行。後來s1取消了訂閱,Observer從_messages刪除了該物件對事件的訂閱,從而第二次提問的時候,s1就不再回答了。