前言
网上关于发布-订阅模式的文章已经有很多,但雷同的数不胜数,没有意思,再者我觉得网上很多文章说得不够形象生动,让人看得云里雾里的,为此我想分享下我对发布-订阅模式的理解。
发布-订阅模式之旅
发布-订阅这个在很多模式中都很常见,比如 VUE。条条道路通罗马,可能实现的方法不同,但效果是差不多的。在一些异步请求中发布-订阅模式也大有用处。我觉得这个发布-订阅模式其实作用就相当于 VUE 中的钩子函数,只是触发的条件或者时间有所不同。
发布-订阅模式实现了消息的统一分发,是一对多的关系,我只要发布了,所有的关注我的人都会收到通知。在这时我们不妨把发布-订阅模式所成订阅号与订阅者。在本例子中,我们还会给订阅号分分配到不同的领域(命名空间,防止命名冲突)。
现在我们来整理一下:
1、首先我们可以有不同的行业或者说领域(命名空间)
2、不同的领域之间可以有同名的订阅号(订阅关键字)
3、同一个领域的同一个订阅关键字可以有多个订阅者(回调函数)
4、发布历史信息,首个订阅者可以收到历史订阅信息(执行回调函数)
需求有了,下面就是代码了,请看码:
const Event = (function () {
let namespaceCache = {}
let hasTriggerCache = {}
const create = function (spaceKey) {
const args = spaceKey.split(":"); // 提取字符串中的关键字
const _DEFAULT = "default";
if (!args[1]) { // 如果只传了 key 值,则默认使用 default 命名空间
args.unshift(_DEFAULT);
}
const spaceName = args[0]; // 提取命名空间
if (!namespaceCache[spaceName]) { // 如果命名空间不存在,则创建
namespaceCache[spaceName] = {};
}
return args; // 返回参数
}
const listen = function (spaceName, key, fn) {
const keyCache = namespaceCache[spaceName][key]; // 订阅号
if (!keyCache) { // 如果订阅号不存在
namespaceCache[spaceName][key] = [];
}
namespaceCache[spaceName][key].push(fn); // 添加订阅者
if (hasTriggerCache[spaceName]) { // 是否在订阅前就已经发布过此订阅,如果是则直接通知订阅者(回调函数)
const param = hasTriggerCache[spaceName][key];
if (param) {
fn.call(this, param); // 直接通知订阅者
hasTriggerCache[spaceName][key] =
null; // 发布过的订阅只能被后续第一个订阅者接收到,如果不设置为 null 那么只要先发布过的,后面所有的订阅都会收到,这里你可以根据自己的场景来进行修改
}
}
return this;
};
const trigger = function (spaceName, key, param) {
const keyCache = namespaceCache[spaceName][key]; // 订阅号
if (!keyCache || !keyCache.length) { // 如果对应的订阅号没有订阅者,则把归类到历史发布中 hasTriggerCache
if (!hasTriggerCache[spaceName]) {
hasTriggerCache[spaceName] = {};
}
hasTriggerCache[spaceName][key] = param;
return;
}
for (let i = 0, item; item = keyCache[i++];) {
item.call(this, param);
}
return this;
};
const remove = function (spaceName, key, fn) {
const keyCache = namespaceCache[spaceName][key]; // 订阅号
if (!keyCache || !fn) { // 如果没有对应的订阅号或没有订阅者(回调),则直接返回
return;
}
const len = keyCache.length;
for (let i = 0; i < len; i++) {
if (keyCache[i] === fn) {
keyCache.splice(i, 1); // 删除对应的订阅者(回调)
if (keyCache.length === 0) {
delete namespaceCache[spaceName][key];
}
break;
}
}
return this;
}
return {
listen: function (spaceKey, fn) {
const args = create(spaceKey);
args.push(fn);
listen.apply(this, args);
},
trigger: function (spaceKey, param) {
const args = create(spaceKey);
args.push(param);
trigger.apply(this, args);
},
remove: function (spaceKey, fn) {
const args = create(spaceKey);
args.push(fn);
remove.apply(this, args);
},
}
})();
// 测试代码
function a(a) {
console.log(a);
}
function b(b) {
console.log(b);
}
/*** 使用默认命名空间 ***/
Event.listen('click', a);
Event.trigger('click', "先订阅后发布!");
/*** 使用自定义命名空间 ***/
Event.listen('ns1:click', a);
Event.trigger('ns1:click', "先订阅后发布!");
Event.trigger('ns2:click', "先发布后订阅!");
Event.listen('ns2:click', b);
// 删除订阅者
Event.remove('ns1:click', a);
Event.trigger('ns1:click');
用法说明:
直接 Event.方法调用。方法的第一个参数由命名空间:关键字组成。如果只传一个关键字,那么就会挂在默认的命名空间下。trigger() 方法的第二个参数(可选)作为回调的参数传入。
世外桃园
在《JavaScript 设计模式与开发实践》一书有也有类似的实现代码,但实现的方式有所不同,我个人觉得代码实现有点繁琐,所以经过阅读理解后,知道它实现了什么功能,然后按自己的思路写了上面更简洁,易懂的代码。但不得不说,这本书让我学到了很多,见识了不少,也推荐您入手一本,我没有收任何广告费,只是觉得这本书还不错,仅此而已。不跑题,下面把书中的代码贴出来,让你过目过目,说不定你会有意外的收获。
码到成功
var Event = (function () {
var global = this,
Event,
_default = "default";
Event = function () {
var _listen,
_trigger,
_remove,
_slice = Array.prototype.slice,
_shift = Array.prototype.shift,
_unshift = Array.prototype.unshift,
namespaceCache = {},
_create,
find,
each = function (ary, fn) {
var ret;
for (var i = 0, l = ary.length; i < l; i++) {
var n = ary[i];
ret = fn.call(n, i, n);
}
return ret;
};
_listen = function (key, fn, cache) {
if (!cache[key]) {
cache[key] = [];
}
cache[key].push(fn);
};
_remove = function (key, cache, fn) {
if (cache[key]) {
if (fn) {
for (var i = cache[key].length; i >= 0; i--) {
if (cache[key][i] === fn) {
cache[key].splice(i, 1);
}
}
} else {
cache[key] = [];
}
}
};
_trigger = function () {
var cache = _shift.call(arguments),
key = _shift.call(arguments),
args = arguments,
_self = this,
ret,
stack = cache[key];
if (!stack || !stack.length) {
return;
}
return each(stack, function () {
return this.apply(_self, args);
});
};
_create = function (namespace) {
var namespace = namespace || _default;
var cache = {},
offlineStack = [],
ret = {
listen: function (key, fn, last) {
_listen(key, fn, cache);
if (offlineStack === null) {
return;
}
if (last === 'last') {
offlineStack.length && offlineStack.pop()();
} else {
each(offlineStack, function () {
this();
});
}
offlineStack = null;
},
one: function (key, fn, last) {
_remove(key, cache);
this.listen(key, fn, last);
},
remove: function (key, fn) {
_remove(key, cache, fn);
},
trigger: function () {
var fn,
args,
_self = this;
_unshift.call(arguments, cache);
args = arguments;
fn = function () {
return _trigger.apply(_self, args);
};
if (offlineStack) {
return offlineStack.push(fn);
}
return fn();
}
}
return namespace ? (namespaceCache[namespace] ? namespaceCache[namespace] : namespaceCache[namespace] = ret) : ret;
};
return {
create: _create,
one: function (key, fn, last) {
var event = this.create();
event.one(key, fn, last);
},
remove: function (key, fn) {
var event = this.create();
event.remove(key, fn);
},
listen: function (key, fn, last) {
var event = this.create();
event.listen(key, fn, last);
},
trigger: function () {
var event = this.create();
event.trigger.apply(this, arguments)
}
};
}();
return Event;
})();
/*** 先发布后订阅 ***/
Event.trigger('click', 1);
Event.listen('click', function (a) {
console.log(a);
})
/*** 使用命名空间 ***/
Event.create("namespace1").listen('click', function (a) {
console.log(a);
});
Event.create("namespace1").trigger('click', 1);
Event.create("namespace2").listen('click', function (a) {
console.log(a);
});
Event.create("namespace2").trigger('click', 2);
这个示例的代码封装得还是很到位的。还有其它一些地方你也可以学习学习,变成自己的编码习惯。