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 订阅发布及实现

相关文章
相关标签/搜索