JavaScript 设计模式之观察者模式与发布订阅模式

前言

在软体工程中,设计模式(design pattern)是对软体设计中广泛存在(反复出现)的各类问题,所提出的解决方案。javascript

设计模式并不直接用来完成程式码的编写,而是描述在各类不一样状况下,要怎么解决问题的一种方案。java

设计模式能使不稳定转为相对稳定、具体转为相对抽象,避免会引发麻烦的紧耦合,以加强软体设计面对并适应变化的能力git

——维基百科github

设计模式是一种软件开发的思想,有益于下降代码的耦合性,加强代码的健壮性。每每在大型项目中用的比较多。设计模式

今天就来介绍一下观察者模式与发布订阅模式。这在解耦中很是实用。数组

什么是观察者模式?

先举一个简单的例子函数

毕业前,不少同窗都会说相似于这样的话:post

“老王,等你结婚了,记得叫我来喝喜酒!”学习

因而有一天你真的要结婚了,且须要举办酒席,这时候你须要通知你的你的那些老友来喝喜酒。因而你拿起了手机给你的那些分布于世界各地的好朋友打起了电话,说告终婚酒席一事。ui

到了酒席那天,有的朋友来了,有的人没来礼却到了,有的呢只有简短的两句祝福,剩下的只有推脱。

这就是观察者模式

在观察者模式中,目标与观察者相互独立,又相互联系:

  • 二者都是相互独立的对对象个体。
  • 观察者在目标对象中订阅事件,目标广播发布事件。

就像以前的例子同样:

  • 老王就是模式中所谓的目标。
  • 同窗们在毕业前说的话就至关于在目标对象上订阅事件。
  • 老王打电话通知朋友就是发布事件。
  • 同窗们各自做出了不一样的行动回应。

这么说咱们的代码就慢慢创建起来了。

首先咱们须要定义两个对象:

  1. 目标对象:Subject
  2. 观察者对象:Observer

而且在目标对象中要存放观察者对象的引用,就像老王要存放同窗的手机好同样,只有存了才能联系嘛。因而咱们有了下面的代码:

function Subject() {
  this.observers = new ObserverList();
}
function ObserverList() {
  this.observerList = [];
}
function Observer() {}
复制代码

对于目标对象中的引用,咱们必须能够动态的控制:

ObserverList.prototype.add = function(obj) {
  return this.observerList.push(obj);
};

ObserverList.prototype.count = function() {
  return this.observerList.length;
};

ObserverList.prototype.get = function(index) {
  if (index > -1 && index < this.observerList.length) {
    return this.observerList[index];
  }
};

ObserverList.prototype.indexOf = function(obj, startIndex) {
  var i = startIndex;

  while (i < this.observerList.length) {
    if (this.observerList[i] === obj) {
      return i;
    }
    i++;
  }
  return -1;
};

ObserverList.prototype.removeAt = function(index) {
  this.observerList.splice(index, 1);
};

Subject.prototype.addObserver = function(observer) {
  this.observers.add(observer);
};

Subject.prototype.removeObserver = function(observer) {
  this.observers.removeAt(this.observers.indexOf(observer, 0));
};
复制代码

这样咱们就能对老王手机联系人进行增、删、查的操做了。

如今咱们就要考虑发布消息的功能函数了。首先必须明确一点:目标对象并不能指定观察者对象作出什么相应的变化。目标对象只有通知的做用。就像老王只能告诉朋友他要办喜酒了,至于朋友接下来怎么办,则全是朋友本身决定的。

因此咱们得写一个目标广播消息的功能函数:

Subject.prototype.notify = function(context) {
  var observerCount = this.observers.count();
  for (var i = 0; i < observerCount; i++) {
    this.observers.get(i).update(context);
  }
};
复制代码

咱们将具体的观察者对象该做出的变化交给了观察者对象本身去处理。这就要求观察者对象须要拥有本身的 update(context)方法来做出改变,同时该方法不该该写在原型链上,由于每个实例化后的 Observer 对象所作的响应都是不一样的,须要独立存储 update(context)方法:

function Observer() {
  this.update = function() {
    // ...
  };
}
复制代码

到此咱们就完成了一个简单的观察者模式的构建。

完整代码:

function ObserverList() {
  this.observerList = [];
}

ObserverList.prototype.add = function(obj) {
  return this.observerList.push(obj);
};

ObserverList.prototype.count = function() {
  return this.observerList.length;
};

ObserverList.prototype.get = function(index) {
  if (index > -1 && index < this.observerList.length) {
    return this.observerList[index];
  }
};

ObserverList.prototype.indexOf = function(obj, startIndex) {
  var i = startIndex;

  while (i < this.observerList.length) {
    if (this.observerList[i] === obj) {
      return i;
    }
    i++;
  }
  return -1;
};

ObserverList.prototype.removeAt = function(index) {
  this.observerList.splice(index, 1);
};

function Subject() {
  this.observers = new ObserverList();
}

Subject.prototype.addObserver = function(observer) {
  this.observers.add(observer);
};

Subject.prototype.removeObserver = function(observer) {
  this.observers.removeAt(this.observers.indexOf(observer, 0));
};

Subject.prototype.notify = function(context) {
  var observerCount = this.observers.count();
  for (var i = 0; i < observerCount; i++) {
    this.observers.get(i).update(context);
  }
};

// The Observer
function Observer() {
  this.update = function() {
    // ...
  };
}
复制代码

什么是发布订阅模式?

先举个简单的例子:

咱们生活中,特别是在一线城市打拼的年轻人,与租房的联系再密切不过了。同时咱们的身边也有不少租房中介。

某天路人甲须要租一套三室一厅一厨一卫的房,他找到了中介问了问有没有。中介看了看发现并无他要的房型,因而和路人甲说:“等有房东提供了此类房型的时候再联系你。”因而你就回去等消息了。

有一天,某一位房东将本身多余的房屋信息以及图片整理好发给中介,中介看了看,这不就是路人甲要的房型吗。因而立马打电话让路人甲看房。最终撮合了一单生意。

这就是发布订阅模式

能够看出,在发布订阅模式中最重要的是 Topic/Event Channel (Event)对象。咱们能够简单的称之为“中介”。

在这个中介对象中既要接受发布者所发布的消息,又要将消息派发给订阅者。因此中介还应该按照不一样的事件储存相应的订阅者信息。

首先咱们先会给中介对象的每一个订阅者对象一个标识,每当有一个新的订阅者订阅事件的时候,咱们就给一个 subUid。

咱们先来写一下中介对象(pubsub):

var pubsub = {};
(function(myObject) {
  var topics = {};
  var subUid = -1;

  myObject.publish = function() {};

  myObject.subscribe = function() {};

  myObject.unsubscribe = function() {};
})(pubsub);
复制代码

这里咱们用了工厂模式来建立咱们的中介对象。

咱们先把订阅功能实现:

首先咱们必须认识到 topics 对象将存放着以下类型的数据:

topics = {
  topicA: [
    {
      token: subuid,
      function: func
    },
  	...
  ],
  topicB: [
    {
      token: subuid,
      function: func
    },
  	...
  ],
  ...
}
复制代码

对于 topics 对象,存放在许多不一样的事件名称(topicA...),对于每个事件都有指定的一个数组对象用以存放订阅该事件的订阅对象及发生事件以后做出的响应。

因此当有订阅对象在中介中订阅事件时:

myObject.subscribe = function(topic, func) {
  //若是不存在相应事件就建立一个
  if (!topics[topic]) {
    topics[topic] = [];
  }
  //将订阅对象信息记录下来
  var token = (++subUid).toString();
  topics[topic].push({
    token: token,
    func: func
  });
  //返回订阅者标识,方标在取消订阅的时候使用
  return token;
};
复制代码

接下来咱们来实现取消订阅的功能:

咱们只须要遍历 topics 各个事件中的对象便可。

myObject.unsubscribe = function(token) {
  for (var m in topics) {
    if (topics[m]) {
      for (var i = 0, j = topics[m].length; i < j; i++) {
        if (topics[m][i].token === token) {
          topics[m].splice(i, 1);
          return token;
        }
      }
    }
  }
  return this;
};
复制代码

剩下的就是发布事件的实现了:

咱们只须要给定事件名称 topic 和相应的参数便可,找到相应事件所对应的订阅者列表,遍历调用列表中的方法。

myObject.publish = function(topic, args) {
  if (!topics[topic]) {
    return false;
  }
  var subscribers = topics[topic],
    len = subscribers ? subscribers.length : 0;
  while (len--) {
    subscribers[len].func(args);
  }
  return this;
};
复制代码

至此,咱们的中介对象就完成了。在发布订阅模式中咱们没必要在乎发布者和订阅者。

完整代码:

var pubsub = {};

(function(myObject) {
  var topics = {};
  var subUid = -1;

  myObject.publish = function(topic, args) {
    if (!topics[topic]) {
      return false;
    }
    var subscribers = topics[topic],
      len = subscribers ? subscribers.length : 0;
    while (len--) {
      subscribers[len].func(args);
    }
    return this;
  };

  myObject.subscribe = function(topic, func) {
    if (!topics[topic]) {
      topics[topic] = [];
    }
    var token = (++subUid).toString();
    topics[topic].push({
      token: token,
      func: func
    });
    return token;
  };

  myObject.unsubscribe = function(token) {
    for (var m in topics) {
      if (topics[m]) {
        for (var i = 0, j = topics[m].length; i < j; i++) {
          if (topics[m][i].token === token) {
            topics[m].splice(i, 1);
            return token;
          }
        }
      }
    }
    return this;
  };
})(pubsub);
复制代码

两者的区别和联系

区别:

  1. 观察者模式中须要观察者对象本身定义事件发生时的相应方法。
  2. 发布订阅模式者在发布对象和订阅对象之中加了一个中介对象。咱们不须要在意发布者对象和订阅者对象的内部是什么,具体响应时间细节所有由中介对象实现。

联系:

  1. 两者都下降了代码的耦合性。
  2. 都具备消息传递的机制,以数据为中心的设计思想。

实战

这里须要一点模板引擎的知识,关于模板引擎能够看我以前发的一篇文章:《手撸 JavaScript 模板引擎》

假如咱们有以下模板须要渲染:

var template = `<span><% this.value %></span>`;
复制代码

该模板依赖的数据源以下:

var data = {
  value: 0
};
复制代码

现倘若 data 中的 value 时动态的,每隔一秒加 1。

setInterval(function() {
  data.value++;
}, 1000);
复制代码

同时咱们也要在页面上发生变化,这时你可能写出以下代码:

setInterval(function() {
  data.value++;
  document.body.innerHTML = TemplateEngine(template, data);
}, 1000);
复制代码

咱们能够对比一下发布订阅模式的实现:

var template = `<span><% this.value %></span>`;
var data = {
  value: 0
};
function render() {
  document.body.innerHTML = TemplateEngine(template, data);
}
window.onload = function() {
  render();
  pubsub.subscribe("change", render);
  setInterval(function() {
    data.value++;
    pubsub.publish("change");
  }, 1000);
};
复制代码

前者彷佛看起来很简单明了,可是:

  1. 不一样功能紧密耦合,若是之后要修改该功能,极可能牵一发而动全身。
  2. 每每实际开发中咱们的订阅者不止一个,发布者的消息也不止一个,远远比这个例子的逻辑复杂的多。剪不断,理还乱。

相比之下,发布订阅模式就显得逻辑清晰,已于维护,值得细细体味。

值得一提:事件监听的实现

事件监听是咱们常常用到的功能,其实它的实现就是源自于发布订阅模式,不信你看:

subject.addEventListener("click", () => {
  //...
});
复制代码

这就是在订阅一个事件的调用。

其实观察者模式与发布订阅模式与咱们息息相关!😁

-EFO-


笔者专门在 github 上建立了一个仓库,用于记录平时学习全栈开发中的技巧、难点、易错点,欢迎你们点击下方连接浏览。若是以为还不错,就请给个小星星吧!👍


2019/04/28

AJie

相关文章
相关标签/搜索