關於觀察者模式與發佈/訂閱模式,很多大神都有帖子對他們作出瞭解釋,可是不少文章都將二者混在了一塊兒,認爲他們就是同一種模式,實際上這二者仍是有些差別的,因此本文就從我在谷歌的查閱和我的的理解,來仔細講講這兩種模式,已經他們的一些應用場景。javascript
官方給出的觀察者模式的解釋是這樣的:html
定義對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,全部依賴於它的對象都獲得通知並被自動更新。vue
觀察者模式實現的,其實就是當目標對象的某個屬性發生了改變,全部依賴着目標對象的觀察者都將接到通知,作出相應動做。 因此在目標對象的抽象類裏,會保存一個觀察者序列。當目標對象的屬性發生改變生,會從觀察者隊列裏取觀察者調用各自的方法。java
下面經過一張圖來看一下觀察者模式的實現。node
class Subject {
let observers = [];
let state;
getState() {
return this.state;
}
setState(state) {
this.state = state;
notifyAllObservers();
}
attach(observer){
observers.push(observer);
}
notifyAllObservers(){
for (observer in observers) {
observer.update();
}
}
}
class Observer {
let subject;
update();
}
class BinaryObserver extends Observer {
constructor(subject) {
super();
subject.attach(this);
}
update() {
console.log("Binary");
}
}
class OctalObserver extends Observer {
constructor(subject) {
super();
subject.attach(this);
}
update() {
console.log("Octal");
}
}
var subject = new Subject();
var binaryObserver = new BinaryObserver(subject);
var octalObserver = new OctalObserver(subject);
subject.setState(15);
//Binary
//Octal
複製代碼
在不少文章裏講到的觀察者模式,其實說的都是發佈訂閱模式,那麼他們的差異到底在哪裏呢,讓咱們一點點往下看。 維基中對於發佈/訂閱是這樣描述的:設計模式
發佈-訂閱是一種消息範式,消息的發送者(稱爲發佈者)不會將消息直接發送給特定的接收者(稱爲訂閱者)。而是將發佈的消息分爲不一樣的類別,無需瞭解哪些訂閱者(若是有的話)可能存在。一樣的,訂閱者能夠表達對一個或多個類別的興趣,只接收感興趣的消息,無需瞭解哪些發佈者(若是有的話)存在。緩存
也就是說,發佈/訂閱模式和觀察者最大的差異就在於消息是否經過一箇中間類進行轉發。bash
由上,咱們就能夠得出這二者的區別了:服務器
同步
的場景,而發佈/訂閱模式大多用於異步
場景,例如消息隊列。到這裏,確定會有小夥伴問,爲何沒有發佈/訂閱模式的代碼實例。其實在不少JS框架中,都採用發佈/訂閱模式進行了很多設計,下面咱們就從Vue和Node來深刻講一講關於發佈/訂閱的使用。網絡
Vue中使用到發佈/訂閱模式最經典的兩塊實現就是數據雙向綁定
和父子組件通訊
。
vue數據雙向綁定是經過數據劫持結合發佈者-訂閱者模式的方式來實現的。 具體實現數據雙向綁定會須要三個步驟:
Vue中,利用 Object.defineProperty()
實現數據劫持,監聽到數據的變化。
Object.defineProperty(data, key, {
set: function (value) {
//...
},
get: function () {
//...
}
})
複製代碼
Observer是一個數據監聽器,用來監聽全部的屬性。
function Observer(data) {
this.data = data;
this.walk(data);
}
Observer.prototype = {
walk: function(data) {
var self = this;
//遍歷對象,得到對象全部屬性的監聽
Object.keys(data).forEach(function(key) {
self.defineReactive(data, key, data[key]);
});
},
defineReactive: function(data, key, val) {
var dep = new Dep();
// 遞歸遍歷全部子屬性
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function getter () {
if (Dep.target) {
// 在這裏添加一個訂閱者,有關Dep.target的得到,會在watcher中實現
dep.addSub(Dep.target);
}
return val;
},
// setter,若是對一個對象屬性值改變,就會觸發setter中的dep.notify(),通知watcher(訂閱者)數據變動,執行對應訂閱者的更新函數,來更新視圖。
set: function setter (newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的話,進行監聽
childObj = observe(newVal);
dep.notify();
}
});
}
};
function observe(value, vm) {
if (!value || typeof value !== 'object') {
return;
}
return new Observer(value);
};
// 消息訂閱器Dep,訂閱器Dep主要負責收集訂閱者,而後在屬性變化的時候執行對應訂閱者的更新函數
function Dep () {
this.subs = [];
}
Dep.prototype = {
/**
* [訂閱器添加訂閱者]
* @param {[Watcher]} sub [訂閱者]
*/
addSub: function(sub) {
this.subs.push(sub);
},
// 通知訂閱者數據變動
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
Dep.target = null;
複製代碼
watcher就是一個訂閱者,裏面包含了添加訂閱者到消息隊列和接收響應發佈者的通知。
function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
this.value = this.get(); // 將本身添加到訂閱器的操做
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function() {
Dep.target = this; // 緩存本身
var value = this.vm.data[this.exp] // 強制執行監聽器裏的get函數
Dep.target = null; // 釋放本身
return value;
}
};
複製代碼
參數解釋:
function SelfVue (data, el, exp) {
this.data = data;
observe(data);
el.innerHTML = this.data[exp]; // 初始化模板數據的值
new Watcher(this, exp, function (value) {
el.innerHTML = value;
});
return this;
}
<body>
<h1 id="name">{{name}}</h1>
</body>
<script src="js/observer.js"></script>
<script src="js/watcher.js"></script>
<script src="js/index.js"></script>
<script type="text/javascript">
var ele = document.querySelector('#name');
var selfVue = new SelfVue({
name: 'hello world'
}, ele, 'name');
window.setTimeout(function () {
console.log('name值改變了');
selfVue.data.name = 'canfoo';
}, 2000);
</script>
複製代碼
其實到這裏咱們就已經實現了vue的數據雙向綁定,從這個綁定過程,咱們也很明確看到發佈/訂閱模式是如何起做用的。 本文主要圍繞兩種設計模式展開,有關compile解析節點的部分,在這裏就不作細講,感興趣的小夥伴能夠繼續深刻源碼探究。
Vue的父子組件通訊也用到了發佈/訂閱模式。
$on
訂閱觀察特定事件$emit
將變化廣播給其餘訂閱觀察對應事件的組件,並調用他們的方法Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
}
return vm
}
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
for (let i = 0, l = cbs.length; i < l; i++) {
cbs[i].apply(vm, args)
}
}
return vm
}
複製代碼
Node中有一個EventEmiter模塊,其消息機制採用的就是發佈/訂閱思想,下面咱們來手寫一個EventEmiter類。
class EvenEmiter{
construct() {
this._events = {};
this.defaultMaxListener = 10;
}
setMaxListner(n) {
this._maxListeners = n;
}
getMaxListener() {
return this._maxListeners ? this.maxListeners : this.defaultMaxListeners;
}
once(eventName, callback) {
wrap(...args) {
callback(...args);
this.removeListener(eventName,callback);
}
wrap.cb = callback;
this.on(eventName, wrap);
}
on(eventName, callback) {
if (!this._events) {
this._events = {}
}
if (this._events[eventName]) {
this._events[eventName].push(callback);
}
else {
this._events[eventName] = [callback];
}
}
emit(eventName) {
if (this._events[eventName]) {
this._events[eventName].forEach((fn) => {
fn()
});
}
}
removeListener(eventName, callback) {
if (this._events[eventName]) {
this._events = this._events.filter(fn => {
return fn !== callback;
})
}
}
addEvnetListener(eventName, callback) {
this.on(eventName, callback);
}
}
複製代碼
以上就是有關觀察者模式和發佈/訂閱模式的所有內容,若是有補充和有錯的地方,歡迎你們留言。
參考連接: vue的雙向綁定原理及實現 node 訂閱發佈及實現