定义

发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

优点

第一点说明发布—订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。 比如,我们可以订阅 ajax 请求的 error、succ 等事件。或者如果想在动画的每一帧完成之后做一些事情,那我们可以订阅一个事件,然后在动画的每一帧完成之后发布这个事件。在异步编程中使用发布—订阅模式,我们就无需过多关注对象在异步运行期间的内部状态,而只需要订阅感兴趣的事件发生点。
第二点说明发布—订阅模式可以取代对象之间硬编码的通知机制,一个对象不用再显式地调 用另外一个对象的某个接口。发布—订阅模式让两个对象松耦合地联系在一起,虽然不太清楚彼 此的细节,但这不影响它们之间相互通信。当有新的订阅者出现时,发布者的代码不需要任何修改;同样发布者需要改变时,也不会影响到之前的订阅者。只要之前约定的事件名没有变化,就可以自由地改变它们。

JavaScript中的运用

JavaScript 本身也是一门基于事件驱动的语言,所以发布—订阅模式的运用相当广泛,如在DOM上绑定事件就是对这种模式的运用,我们只需要将事件绑定到对应DOM上后,我们将不需要关注用户何时去触发,而是关注触发后的动作。在事件触发后就DOM将会通知执行我们绑定的事件。我们常用的vue框架也是使用该模式实现了对数据的双向绑定,在使用JavaScript逻辑对需要监听的数据进行改变时,会触发改变页面显示的解析方法,同理,在页面上对监听的数据进行改变是也会去改变对应的JavaScript里的变量值,详细分析点击vue2中对发布-订阅模式的运用vue3中对发布-订阅模式的运用

必须先订阅再发布吗?

上面说到该模式广泛应用于异步编程中,对于异步里无法判断触发时间,因此会出现还未加载执行完毕订阅相关代码时发布代码执行了,造成发布消息的代码成为无效代码。对于这种情况必须编写该模块时进行判断。如创建一个存储离线事件的堆栈,当事件发布的时候,如果此时还 没有订阅者来订阅这个事件,我们暂时把发布事件的动作包裹在一个函数里,这些包装函数将被 存入堆栈中,等到终于有对象来订阅此事件的时候,我们将遍历堆栈并且依次执行这些包装函数,也就是重新发布里面的事件。

// 对于自定义对象,实现发布-订阅模式
let Event = (function () {
let Event = null;
let _default = 'default';
Event = function () {
let _listen = null;
let _trigger = null;
let _remove = null;
let _shift = Array.prototype.shift;
let _unshift = Array.prototype.unshift;
let namespaceCache = {}; // 使用该对象将命名空间缓存起来
let _create = null;
// 用户当用户触发是进行事件遍历,而进行对以订阅者进行发布发布
each = function (ary, fn) {
let ret = null;
for (let i = 0; i < ary.length; i++) {
let n = ary[i];
ret = fn.call(n, i, n);
}
return ret;
};
// 添加监听
_listen = function (key, fn, cache) {
console.log(cache,"输出cache")
if (!cache[key]) cache[key] = [];
cache[key].push(fn);
};
// 触发
_trigger = function () {
console.log({...arguments},'argumentsargumentsarguments')
let cache = _shift.call(arguments);
console.log(cache,'cachecachecachecache')
let key = _shift.call(arguments);
let args = arguments;
let _self = this;
let stack = cache[key];
if (!stack || !stack.length) {
return;
}
return each(stack, function () {
return this.apply(_self, args)
})
};
// 如果已经创建该命名空间则返回该命名空间的发布订阅对象,否则将使用ret创建的新对象
_create = function (namespace) {
var namespace = namespace || _default;
let cache = {};
let offlineStack = [];
let 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 () {
console.log(this)
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 () {
let fn = null;
let args = arguments;
let _self = this;
_unshift.call(arguments, cache);
args = arguments;
fn = function () {
return _trigger.apply(_self, args);
};
if (offlineStack) {
return offlineStack.push(fn);
}
return fn()
}
};
// 如果命名空间在缓存中,则使用已经存起来的对象,如果不存在则使用_default的命名空间
return namespaceCache[namespace] ? namespaceCache[namespace] : namespaceCache[namespace] = ret;
};
return {
create: _create, // 使用命名空间形式
one: function (key, fn, last) {
let event = this.create();
event.one(key, fn, last);
},
remove: function (key, fn) {
let event = this.create();
event.remove(key, fn);
},
listen: function (key, fn, last) {
let event = this.create();
event.listen(key, fn, last)
},
trigger: function () {
let event = this.create();
event.trigger.apply(this, arguments)
}
}
}();
return Event;
})();
// 如果命名空间在缓存中,则使用已经存起来的对象,如果不存在则使用_default的命名空间

Event.trigger('click', 1);
Event.listen('click', function (a) {
console.log(a);
})
Event.listen('click', function (a) {
console.log(a);
})
// 使用命名空间=================================
Event.create('namespace1').listen('click', function (a) {
console.log(a); // 输出:1
});
Event.create('namespace1').listen('click', function (a) {
console.log(a,'大萨达萨达'); // 输出:1
});
Event.create('namespace1').listen('click', function (a) {
console.log(a,'禅道上的就后端数据肯定会'); // 输出:1
});
Event.create('namespace2').listen('click', function (a) {
console.log(a); // 输出:2
});
Event.create('namespace1').trigger('click', 123);

小结

发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。它的应用非常 广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。但是不能过度使用该模式,因此在创建后,订阅模式代码肯定会占用一定内存,不论是否有消息进行发布。而且该模式会弱化对象之间的关系,将会使代码可读性降低,进而导致程序难以跟踪维护和理解。