likes
comments
collection
share

JavaScript设计模式 ---- 从买房子看(发布-订阅者)模式

作者站长头像
站长
· 阅读数 72

不论是在程序世界里还是现实生活中,发布—订阅模式的应用都非常之广泛。

举个例子🌰:

玛卡巴卡在网上看上一个小推车,但联系到卖家后,发现小推车卖光了,但是玛卡巴卡对这个小推又非常喜欢,所以就联系卖家,问卖家什么时候有货,卖家告诉她,要等一个星期后才有货,卖家告诉玛卡巴卡,要是你喜欢的话,你可以收藏我们的店铺,等有货的时候再通知你,所以玛卡巴卡收藏了此店铺。唔西迪西和依古比古看了图片之后,也喜欢这个小推车,也收藏了该店铺;等来货的时候就依次会通知他们。

你看这是一个典型的发布订阅模式,卖家是属于发布者,玛卡巴卡和唔西迪西等属于订阅者,订阅该店铺,卖家作为发布者,当小推车到了的时候,会依次通知玛卡巴卡,唔西迪西等,使用旺旺等工具给他们发布消息。

JavaScript设计模式 ---- 从买房子看(发布-订阅者)模式 再来一个栗子🌰:

你最近看上了一套北京一环的房子,到了售楼处之后才被告知,该楼盘的房子早已售罄。好在售楼小姐姐告诉你,不久后还有一些尾盘推出,开发商正在办理相关手续,手续办好后便可以购买。但到底是什么时候,目前还没有人能够知道。

于是你把电话号码留在了售楼处,售楼小姐姐非常热情的答应你,新楼盘一推出就马上发信息通知你,毕竟她的工资来源于你的提成。当然了,除了你,还有你二狗子兄弟,你三蛋兄弟也想和你买一个小区的房子,你们的电话号码都被记在售楼处的花名册上,新楼盘推出的时候,售楼小姐姐会翻开花名册,翻阅上面的电话号码,依次发送短信来通知你们。

发送短信通知就是一个典型的发布—订阅模式,你,二狗子,三蛋等购买者都是订阅者,你们订阅了房子开售的消息。售楼处作为发布者,会在合适的时候遍历花名册上的电话号码,依次给购房者发布消息。

JavaScript设计模式 ---- 从买房子看(发布-订阅者)模式

发布—订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。比如,可以订阅 ajax 请求的 error、succ 等事件。或者如果想在动画的每一帧完成之后做一些事情,那我们可以订阅一个事件,然后在动画的每一帧完成之后发布这个事件。在异步编程中使用发布—订阅模式,我们就无需过多关注对象在异步运行期间的内部状态,而只需要订阅感兴趣的事件发生点。

发布—订阅模式可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。发布—订阅模式让两个对象松耦合地联系在一起,虽然不太清楚彼此的细节,但这不影响它们之间相互通信。当有新的订阅者出现时,发布者的代码不需要任何修改;同样发布者需要改变时,也不会影响到之前的订阅者。只要之前约定的事件名没有变化,就可以自由地改变它们。

就像上面的例子,购房者和售楼处之间不再强耦合在一起,当有新的购房者出现时,他只需把手机号码留在售楼处,售楼处不关心购房者的任何情况,不管购房者是男是女还是一只猴子。 而售楼处的任何变动也不会影响购买者,比如售楼小姐姐离职,售楼处从一楼搬到二楼,这些 改变都跟购房者无关,只要售楼处记得发短信这件事情。

DOM 节点上面绑定过事件函数,那就使用了发布—订阅模式

document.body.addEventListener( 'click', function(){ 
 alert(2); 
}, false ); 
document.body.click(); // 模拟用户点击

我们没办法预知用户将在什么时候点击。所以我们订阅 document.body 上的 click 事件,当 body 节点被点击时,body 节点便会向订阅者发布这个消息。

随意增加或者删除订阅者,增加任何订阅者都不会影响发布者代码的编写:

document.body.addEventListener( 'click', function(){ 
 alert(2); 
}, false ); 

document.body.addEventListener( 'click', function(){ 
 alert(3); 
}, false ); 

document.body.addEventListener( 'click', function(){ 
 alert(4); 
}, false ); 

document.body.click(); // 模拟用户点击

Vue 中的 on和on 和 onemit 方法,使用了发布-订阅模式。

function eventsMixin (Vue) {
  var hookRE = /^hook:/;
  Vue.prototype.$on = function (event, fn) {
    var vm = this;
    if (Array.isArray(event)) {
      for (var i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn);
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn);
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {
        vm._hasHookEvent = true;
      }
    }
    return vm
  };

  Vue.prototype.$once = function (event, fn) {
    var vm = this;
    function on () {
      vm.$off(event, on);
      fn.apply(vm, arguments);
    }
    on.fn = fn;
    vm.$on(event, on);
    return vm
  };

  Vue.prototype.$off = function (event, fn) {
    var vm = this;
    // all
    if (!arguments.length) {
      vm._events = Object.create(null);
      return vm
    }
    // array of events
    if (Array.isArray(event)) {
      for (var i$1 = 0, l = event.length; i$1 < l; i$1++) {
        vm.$off(event[i$1], fn);
      }
      return vm
    }
    // specific event
    var cbs = vm._events[event];
    if (!cbs) {
      return vm
    }
    if (!fn) {
      vm._events[event] = null;
      return vm
    }
    // specific handler
    var cb;
    var i = cbs.length;
    while (i--) {
      cb = cbs[i];
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1);
        break
      }
    }
    return vm
  };

  Vue.prototype.$emit = function (event) {
    var vm = this;
    if (process.env.NODE_ENV !== 'production') {
      var lowerCaseEvent = event.toLowerCase();
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(
          "Event \"" + lowerCaseEvent + "\" is emitted in component " +
          (formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " +
          "Note that HTML attributes are case-insensitive and you cannot use " +
          "v-on to listen to camelCase events when using in-DOM templates. " +
          "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"."
        );
      }
    }
    var cbs = vm._events[event];
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs;
      var args = toArray(arguments, 1);
      var info = "event handler for \"" + event + "\"";
      for (var i = 0, l = cbs.length; i < l; i++) {
        invokeWithErrorHandling(cbs[i], vm, args, vm, info);
      }
    }
    return vm
  };
}

实现发布—订阅模式

  • 首先要指定谁充当发布者(比如售楼处);
  • 然后给发布者添加一个缓存列表,用于存放回调函数以便通知订阅者(售楼处的花名册);
  • 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数(遍历花名册,挨个发短信)。

另外,我们还可以往回调函数里填入一些参数,订阅者可以接收这些参数。比如售楼处可以在发给订阅者的短信里加上房子的单价、面积、容积率等信息,订阅者接收到这些信息之后可以进行各自的处理。

class EventEmitter {
  constructor() {
    this.events = {};
  }
  // 实现订阅
  addListener(type, callBack) {
    if (!this.events) this.events = Object.create(null);

    if (!this.events[type]) {
      this.events[type] = [callBack];
    } else {
      this.events[type].push(callBack);
    }
  }
  // 删除订阅
  off(type, callBack) {
    if (!this.events[type]) return;
    this.events[type] = this.events[type].filter(item => {
      return item !== callBack;
    });
  }
  // 只执行一次订阅事件
  once(type, callBack) {
    function fn() {
      callBack();
      this.off(type, fn);
    }
    this.addListener(type, fn);
  }
  // 触发事件
  emit(type, ...rest) {
    this.events[type] && this.events[type].forEach(fn => fn.apply(this, rest));
  }
}
const event = new EventEmitter();

const user1 = (content) => {
  console.log('用户1订阅了:', content)
};

event.addListener("sale", user1);

event.emit("sale", "大明贵妇");

event.off("sale", user1);
console.log(event);

const user2 = () => {
  console.log('用户2订阅只想订阅一次');
}
event.once("type", user2);
event.emit("type");
event.emit("type");

必须先订阅再发布吗?

一般发布—订阅模式,都是订阅者必须先订阅一个消息,随后才能接收到发布者发布的消息。如果把顺序反过来,发布者先发布一条消息,而在此之前并没有对象来订阅它,这条消息无疑将消失在宇宙中。。。。。

const event = new EventEmitter();

const user1 = (content) => {
  console.log('用户1订阅了:', content)
};

const user2 = (content) => {
  console.log('用户2订阅了:', content)
};

event.emit("sale", "大明贵妇");

event.addListener("sale", user1);
event.addListener("sale", user2);

为了满足这个需求,需要建立一个存放离线事件的堆栈,当事件发布的时候,如果此时还没有订阅者来订阅这个事件,我们暂时把发布事件的动作包裹在一个函数里,这些包装函数将被存入堆栈中,等到终于有对象来订阅此事件的时候,我们将遍历堆栈并且依次执行这些包装函数,也就是重新发布里面的事件。当然离线事件的生命周期只有一次,所以刚才的操作只能进行一次。

也可以参考一下 三元大佬 的写法:

function EventEmitter() {
  this.events = new Map();
};

// once 参数表示是否只是触发一次
const wrapCallback = (fn, once = false) => ({ callback: fn, once });

EventEmitter.prototype.addListener = function (type, fn, once = false) {
  let handler = this.events.get(type);
  if (!handler) {
    // 为 type 事件绑定回调
    this.events.set(type, wrapCallback(fn, once));
  } else if (handler && typeof handler.callback === 'function') {
    // 目前 type 事件只有一个回调
    this.events.set(type, [handler, wrapCallback(fn, once)]);
  } else {
    // 目前 type 事件回调数 >= 2
    handler.push(wrapCallback(fn, once));
  }
};

EventEmitter.prototype.off = function (type, listener) {
  let handler = this.events.get(type);
  if (!handler) return;
  if (!Array.isArray(handler)) {
    if (handler.callback === listener.callback) this.events.delete(type);
    else return;
  }
  for (let i = 0; i < handler.length; i++) {
    let item = handler[i];
    if (item.callback === listener) {
      // 删除该回调,注意数组塌陷的问题,即后面的元素会往前挪一位。i 要 -- 
      handler.splice(i, 1);
      i--;
      if (handler.length === 1) {
        // 长度为 1 就不用数组存了
        this.events.set(type, handler[0]);
      }
    }
  }
};

EventEmitter.prototype.once = function (type, fn) {
  this.addListener(type, fn, true);
}

EventEmitter.prototype.emit = function (type, ...args) {
  let handler = this.events.get(type);
  if (!handler) return;
  if (Array.isArray(handler)) {
    // 遍历列表,执行回调
    handler.map(item => {
      item.callback.apply(this, args);
      // 标记的 once: true 的项直接移除
      if (item.once) this.off(type, item);
    })
  } else {
    // 只有一个回调则直接执行
    handler.callback.apply(this, args);
  }
  return true;
};

EventEmitter.prototype.removeAllListener = function (type) {
  let handler = this.events.get(type);
  if (!handler) return;
  else this.events.delete(type);
};
function user1(content) {
  console.log('用户1订阅了公众号: ', content);
};

function user2(content) {
  console.log('用户2订阅了公众号: ', content);
};

let e = new EventEmitter();

e.addListener('type', user1);
e.addListener('type', user2);

e.off('type', user1);

console.log(e);

function f() {
  console.log("我只想订阅这一个公众号");
};
e.once('type_1', f);
e.emit('type', '大明贵妇');
e.emit('type_1');
e.removeAllListener('type');
e.emit('type');

Node中 Event 模块 的源码,里面对各种细节和边界情况做了详细的处理。这其中定义了 defaultMaxListeners(订阅者最多数量),listenerCount(订阅者数量),addErrorHandlerIfEventEmitter(错误处理)之类的。

function EventEmitter() {
  EventEmitter.init.call(this);
}
module.exports = EventEmitter;
module.exports.once = once;

EventEmitter.EventEmitter = EventEmitter;

EventEmitter.prototype._events = undefined;
EventEmitter.prototype._eventsCount = 0;
EventEmitter.prototype._maxListeners = undefined;

var defaultMaxListeners = 10;

function checkListener(listener) {
  //....
}

Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
  //....
});

EventEmitter.init = function () {

  if (this._events === undefined ||
    this._events === Object.getPrototypeOf(this)._events) {
    this._events = Object.create(null);
    this._eventsCount = 0;
  }

  this._maxListeners = this._maxListeners || undefined;
};

EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
  //....
};

function _getMaxListeners(that) {
  //...
}

EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
  //...
};

EventEmitter.prototype.emit = function emit(type) {
  //...
};

function _addListener(target, type, listener, prepend) {
  //...
}

EventEmitter.prototype.addListener = function addListener(type, listener) {
  //...
};

EventEmitter.prototype.on = EventEmitter.prototype.addListener;

EventEmitter.prototype.prependListener = function prependListener(type, listener) {
  //...
};

function onceWrapper() {
  //...
}

function _onceWrap(target, type, listener) {
  //...
}

EventEmitter.prototype.once = function once(type, listener) {
  //...
};

EventEmitter.prototype.prependOnceListener = function prependOnceListener(type, listener) {
  //...
};

EventEmitter.prototype.removeListener = function removeListener(type, listener) {
  //...
}

EventEmitter.prototype.off = EventEmitter.prototype.removeListener;

EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) {
  //...
}

function _listeners(target, type, unwrap) {
  //...
}

EventEmitter.prototype.listeners = function listeners(type) {
  //...
};

EventEmitter.prototype.rawListeners = function rawListeners(type) {
  //...
};

EventEmitter.listenerCount = function (emitter, type) {
  //...
};

EventEmitter.prototype.listenerCount = listenerCount;

function listenerCount(type) {
  //...
}

EventEmitter.prototype.eventNames = function eventNames() {
  //...
};

function once(emitter, name) {
  //...
}

function addErrorHandlerIfEventEmitter(emitter, handler, flags) {
  //...
}

function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
  //...
}

参考: 《JavaScript 设计模式与开发实践》 Javascript中理解发布--订阅模式

转载自:https://juejin.cn/post/7136511790262779935
评论
请登录