传统的发布订阅又可以叫做观察者模式,是松耦合的一对多关系。发布者持有订阅者的方法,发布者发布通知的时候会直接调用订阅者的方法。而全局的发布-订阅模式有一个调度中心(中间对象),中间对象保存了订阅者的方法,发布者发布通知的时候,由调度中心调用订阅者的方法,是一种完全解耦的多对多关系。
该模式有两种模型:推模型和拉模型。区别在于,推模型在trigger的时候会将所有信息推送给订阅者,而拉模型想要传递给订阅者的消息只有通过订阅者在listened之后请求特定的接口。无疑拉模型要复杂些。JS中常用的是推模型。
这种模式可以将订阅者的主动询问更新方式转换为被动接受更新通知,效率会更高,同时解耦发布者与订阅者的关系,更易维护和扩展。过度的使用会将发布者与订阅者的联系隐藏的很深,维护起来也很蛋疼。
曾探在书中讲的前两种实现分别是混入实现和全局实现。混入实现比较浪费内存,而且发布者与订阅者是松耦合的关系,并没有完全解耦(订阅者必须知道发布者的名字,体现在代码上就是持有发布者对象)。全局实现就是为了解决这两个问题。
// 通用的事件发布-订阅模式
const event = {
clientListen: {},
listen(key, fn) {
if (!this.clientListen[key]) {
this.clientListen[key] = [];
}
this.clientListen[key].push(fn);
},
trigger() {
const key = [].shift.call(arguments),
args = [].slice.call(arguments),
fns = this.clientListen[key];
if (!fns || fns.length === 0) {
return false;
}
fns.forEach(fn => fn.apply(this, args));
},
remove(key, fn) {
let fns = this.clientListen[key];
if (!fns || fns.length === 0) {
return false;
}
if (!fn) {
fns && (fns.length = 0);
} else {
this.clientListen[key] = fns.filter(_fn => _fn !== fn);
}
},
};
// 混入方式,将 event 对象的属性都抄写到目标对象上
function installEvent(obj) {
Object.keys(event).forEach(property => obj[property] = event[property]);
}
// test code
var salesOffices = {};
installEvent(salesOffices);
salesOffices.listen('squareMeter88', function(price) {
console.log('价格=', price);
});
salesOffices.listen('squareMeter100', function(price) {
console.log('价格=', price);
});
const xiaohong = function(price) {
console.log('价格=', price);
}
salesOffices.listen('squareMeter100', xiaohong);
salesOffices.remove('squareMeter100', xiaohong);
salesOffices.trigger('squareMeter88', 20000);
salesOffices.trigger('squareMeter100', 30000);
// 全局发布-订阅模式
// 解决两个问题:① 订阅者持有发布者对象的引用(弱耦合);② 每个发布者对象都有自己独立的 listen/trigger/clientList。
var Event = (function() {
const clientList = {};
const listen = function(key, fn) {
if (!clientList[key]) {
clientList[key] = [];
}
clientList[key].push(fn);
};
const trigger = function() {
const key = [].shift.call(arguments),
fns = clientList[key];
if (!fns || fns.length === 0) {
return false;
}
fns.forEach(fn => fn.apply(this, arguments));
};
const remove = function(key, fn) {
const fns = clientList[key];
if (!fns || fns.length === 0) {
return false;
}
if (!fn) {
fns && (fns.length = 0);
} else {
clientList[key] = fns.filter(_fn => _fn !== fn);
}
};
return {
listen, trigger, remove,
}
})();
Event.listen('squareMeter88', function(price) {
console.log('价格=', price);
});
Event.trigger('squareMeter88', 8000);
还可以实现先发布后订阅和命名空间功能。使用一个离线事件栈保存发布者的trigger函数的包装函数以及参数,在订阅者listen的时候执行。命名空间就是又套了层IIFE和闭包。
// 给全局发布-订阅对象增加两个功能:
// ① 先发布后订阅能力;② 命名空间
var Event = (function () {
const _default = 'default';
const Event = (function () {
const _slice = Array.prototype.slice,
_shift = Array.prototype.shift,
_unshift = Array.prototype.unshift,
namespaceCache = {};
// 清空执行 offlineStack 里的包装函数
const each = function (ary, fn) {
var ret;
for (let i = 0, l = ary.length; i < l; i++) {
var n = ary[i];
ret = fn.call(n, i, n);
}
return ret;
}
const _listen = function (key, fn, cache) {
if (!cache[key]) {
cache[key] = [];
}
cache[key].push(fn);
}
const _trigger = function () {
const cache = _shift.call(arguments),
key = _shift.call(arguments),
args = arguments,
_self = this,
stack = cache[key];
if (!stack || !stack.length) {
return;
}
return each(stack, function () {
return this.apply(_self, args);
});
}
const _create = function (namespace) {
var namespace = namespace || _default,
cache = {},
offlineStack = [],
ret = {
listen(key, fn, last) {
_listen(key, fn, cache);
if (offlineStack === null) {
return;
}
// 如果 last 传进来等于字符串 'last' 那就只执行 离线事件栈 的 栈顶包装函数
// 否则就调用 each 清空执行 离线事件栈的所有包装函数
if (last === 'last') {
offlineStack.length && offlineStack.pop()();
} else {
each(offlineStack, function () {
this();
});
}
// 将 离线事件栈 置空,这玩意只用一次
offlineStack = null;
},
one(kye, fn, last) {
_remove(key, cache);
this.listen(key, fn, last);
},
remove(key, fn) {
_remove(key, cache, fn);
},
trigger() {
var fn,
args,
_self = this;
_unshift.call(arguments, cache);
args = arguments;
// 将 _trigger 和 参数 包装起来
fn = function () {
return _trigger.apply(_self, args);
};
// 将包装函数存进 offlineStack 离线事件栈里
if (offlineStack) {
return offlineStack.push(fn);
}
return fn();
}
};
// _create 函数仍持有 namespace,就可以在下次调用 create 返回对象的时候定位到自己所属的命名空间了
return namespace ? (
namespaceCache[namespace] ?
namespaceCache[namespace] :
(namespaceCache[namespace] = ret)) :
ret;
}
return {
create: _create,
one(key, fn, last) {
var event = this.create();
event.one(key, fn, last);
},
remove(key, fn) {
var event = this.create();
event.remove(key, fn);
},
listen(key, fn, last) {
var event = this.create();
event.listen(key, fn, last);
},
trigger() {
var event = this.create();
event.trigger.apply(this, arguments);
}
};
})();
return Event;
})();
// 先发布后订阅
Event.trigger('click', 1);
Event.listen('click', function (a) {
console.log(a);
});
// 使用命名空间
var namespace1 = Event.create('namespace1');
var namespace2 = Event.create('namespace2');
namespace1.listen('click', function(a) {
console.log(a);
});
namespace2.listen('click', function(a) {
console.log(a);
});
namespace1.trigger('click', 1);
namespace2.trigger('click', 2);