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 和 on和emit 方法,使用了发布-订阅模式。
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