设计模式--发布-订阅模式

329 阅读4分钟

1.定义

发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状 态发生改变时,所有依赖于它的对象都将得到通知。在 JavaScript 开发中,我们一般用事件模型 来替代传统的发布—订阅模式。

2.发布-订阅模式的通用实现

举个🌰:小明看上了一个房子,到了售楼处发现房子售罄了。然后销售告诉最近有新楼盘要推出,叫小明将电话留下,推出楼盘后好告知小明,实际上不光是小明,小强,小红也是如此,在等待销售的通知中。

const salesOffices = {}; // 定义售楼处
salesOffices.clientList = []; // 缓存列表,存放订阅者的回调函数

// 增加订阅者
salesOffices.listen = function (fn) {
  this.clientList.push(fn); // 订阅的消息添加进缓存列表
};
// 发布消息
salesOffices.trigger = function () {
  for (let i = 0, fn; (fn = this.clientList[i++]); ) {
    fn.apply(this, arguments); // arguments 是发布消息时带上的参数
  }
};

// 传入订阅事件
salesOffices.listen(function (price, squareMeter) {
  // 小明订阅消息
  console.log("价格= " + price);
  console.log("squareMeter= " + squareMeter);
});
salesOffices.listen(function (price, squareMeter) {
  // 小红订阅消息
  console.log("价格= " + price);
  console.log("squareMeter= " + squareMeter);
});
salesOffices.trigger(2000000, 88); // 输出:200 万,88 平方米
salesOffices.trigger(3000000, 110); // 输出:300 万,110 平方米

在此实现了一个非常简单的发布-订阅模式,细心地朋友一看便会发现小明只想购买88平米的房子,但是销售处将110平米的房源信息也通知给了小明,这明显是不太合理的。

const salesOffices = {}; // 定义售楼处
salesOffices.clientList = {}; // 缓存列表,存放订阅者的回调函数

// 增加订阅者
salesOffices.listen = function (fn) {
  if (!this.clientList[key]) {
    // 如果还没有订阅过此类消息,给该类消息创建一个缓存列表
    this.clientList[key] = [];
  }
  this.clientList[key].push(fn); // 订阅的消息添加进消息缓存列表
};
// 发布消息
salesOffices.trigger = function () {
  const key = Array.prototype.shift.call(arguments), // 取出消息类型
    fns = this.clientList[key]; // 取出该消息对应的回调函数集合
  if (!fns || fns.length === 0) {
    // 如果没有订阅该消息,则返回
    return false;
  }
  for (let i = 0, fn; (fn = fns[i++]); ) {
    fn.apply(this, arguments); // (2) // arguments 是发布消息时附送的参数
  }
};

// 传入订阅事件
salesOffices.listen("squareMeter88", function (price) {
  // 小明订阅 88 平方米房子的消息
  console.log("价格= " + price); // 输出: 2000000
});
salesOffices.listen("squareMeter110", function (price) {
  // 小红订阅 110 平方米房子的消息
  console.log("价格= " + price); // 输出: 3000000
});
salesOffices.trigger("squareMeter88", 2000000); // 发布 88 平方米房子的价格
salesOffices.trigger("squareMeter110", 3000000); // 发布 110 平方米房子的价格

3.发布-订阅模式的通用的实现

通过上面的🌰我们知道了如何让售楼处拥有接收事件和订阅的功能,那么如果让其他的售楼处拥有同样的功能,是否需要将上面的代码给复制一份呢?肯定是不需要的。

首先我们可以将发布-订阅功能提取出来放到一个单独的对象内。

const event = {
  clientList: [],
  listen: function (key, fn) {
    if (!this.clientList[key]) {
      this.clientList[key] = [];
    }
    this.clientList[key].push(fn); // 订阅的消息添加进缓存列表
  },
  trigger: function () {
    let key = Array.prototype.shift.call(arguments),
      fns = this.clientList[key];
    if (!fns || fns.length === 0) {
      // 如果没有绑定对应的消息
      return false;
    }
    for (let i = 0, fn; (fn = fns[i++]); ) {
      fn.apply(this, arguments); // arguments 是 trigger 时带上的参数
    }
  },
};

定义一个installEvent函数,这个函数的具体功能是为每个对象动态的安装发布-订阅功能

const installEvent = function (obj) {
  for (let i in event) {
    obj[i] = event[i];
  }
};

测试:为售楼部salesOffices动态添加发布-订阅功能。

const salesOffices = {};
installEvent(salesOffices);
salesOffices.listen("squareMeter88", function (price) {
  // 小明订阅消息
  console.log("价格= " + price);
});
salesOffices.listen("squareMeter100", function (price) {
  // 小红订阅消息
  console.log("价格= " + price);
});
salesOffices.trigger("squareMeter88", 2000000); // 输出:2000000
salesOffices.trigger("squareMeter100", 3000000); // 输出:3000000

4. 取消订阅功能

有的时候我们需要取消事件的订阅,比如小明不想购买房子了,如果此时售楼处依然跟她发短信/打电话,这样显然是不合理的,所以此时需要给小明取消之前订阅的事件,即为event对象增加remove方法。

event.remove = function (key, fn) {
  let fns = this.clientList[key];
  if (!fns) {
    // 如果 key 对应的消息没有被人订阅,则直接返回
    return false;
  }
  if (!fn) {
    // 如果没有传入具体的回调函数,表示需要取消 key 对应消息的所有订阅
    fns && (fns.length = 0);
  } else {
    for (let l = fns.length - 1; l >= 0; l--) {
      // 反向遍历订阅的回调函数列表
      let _fn = fns[l];
      if (_fn === fn) {
        fns.splice(l, 1); // 删除订阅者的回调函数
      }
    }
  }
};
let salesOffices = {};
let installEvent = function (obj) {
  for (let i in event) {
    obj[i] = event[i];
  }
};
installEvent(salesOffices);
salesOffices.listen(
  "squareMeter88",
  (fn1 = function (price) {
    // 小明订阅消息
    console.log("价格= " + price);
  })
);
salesOffices.listen(
  "squareMeter88",
  (fn2 = function (price) {
    // 小红订阅消息
    console.log("价格= " + price);
  })
);
salesOffices.remove("squareMeter88", fn1); // 删除小明的订阅
salesOffices.trigger("squareMeter88", 2000000); // 输出:2000000