JS中觀察者模式與發佈訂閱模式

關於觀察者模式與發佈/訂閱模式,很多大神都有帖子對他們作出瞭解釋,可是不少文章都將二者混在了一塊兒,認爲他們就是同一種模式,實際上這二者仍是有些差別的,因此本文就從我在谷歌的查閱和我的的理解,來仔細講講這兩種模式,已經他們的一些應用場景。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數據雙向綁定是經過數據劫持結合發佈者-訂閱者模式的方式來實現的。 具體實現數據雙向綁定會須要三個步驟:

  • 實現一個監聽器Observer,用來劫持並監聽全部屬性,若是有變更的,就通知訂閱者。
  • 實現一個訂閱者Watcher,每個Watcher都綁定一個更新函數,watcher能夠收到屬性的變化通知並執行相應的函數,從而更新視圖。
  • 實現一個解析器Compile,能夠掃描和解析每一個節點的相關指令(v-model,v-on等指令),若是節點存在v-model,v-on等指令,則解析器Compile初始化這類節點的模板數據,使之能夠顯示在視圖上,而後初始化相應的訂閱者(Watcher)。

數據劫持

Vue中,利用 Object.defineProperty() 實現數據劫持,監聽到數據的變化。

Object.defineProperty(data, key, {
  set: function (value) {
    //...
  },
  get: function () {
    //...
  }
})
複製代碼

實現Observer

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

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;
  }
};
複製代碼

參數解釋:

  • cb:訂閱者綁定的更新函數。
  • vm:Vue實例化的對象。
  • exp:節點的v-model或v-on:click等指令的屬性值。

關聯Observer和Watcher

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的父子組件通訊也用到了發佈/訂閱模式。

  • A組件經過 $on 訂閱觀察特定事件
  • B組件經過 $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中的發佈/訂閱設計

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 訂閱發佈及實現

相關文章
相關標籤/搜索