JavaScript发布订阅模式实现与原理
发布订阅模式(Observer Pattern)定义了一对多的依赖关系,当对象状态改变时自动通知所有订阅者。由于 JavaScript 原生支持回调和匿名函数,实现该模式比 Java 等传统语言更简洁。本文将基于原生 JS 展示如何实现带条件过滤、取消订阅以及离线缓存的事件总线。
简介
发布-订阅模式又称观察者模式,它定义了一种一对多的依赖关系:当一个目标对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。这种模式将「谁来做什么」和「什么时候做」进行了时间上的解耦。
浏览器原生事件的启示
在前端开发中,给 DOM 节点绑定事件是最常见的应用场景。例如:
document.body.addEventListener('click', function () {
console.log('被点击了');
}, false);
document.body.click(); // 模拟触发
当我们向 document.body 注册了 click 事件后,一旦 body 被点击,它就会向所有的订阅者(监听器)发布这个动作。我们可以随意添加多个处理函数:
document.body.addEventListener('click', () => alert(111));
document.body.addEventListener('click', () => alert(222));
虽然可以直接调用元素方法触发自定义事件(如 element.click()),但在跨浏览器兼容场景下,推荐使用标准 API。以下是一个简易的跨浏览器事件派发封装:
let fireEvent = function (element, event) {
if (document.createEventObject) { // IE 兼容
var evt = document.createEventObject();
return element.fireEvent('on' + event, evt);
} else { // 标准浏览器
var evt = document.createEvent('HTMLEvents');
evt.initEvent(event, true, true);
return element.dispatchEvent(evt);
}
};
fireEvent(document, 'shout');
手动实现基础发布订阅器
抽象现实生活中的例子:你要结婚,需要从通讯录中找到所有朋友并挨个打电话通知。这个过程对应的设计要素包括:
- 发布者:你(消息源)。
- 订阅者列表:通讯录(存储需要接收消息的人/函数)。
- 触发机制:遍历列表,依次执行回调。
第一版:简单的事件分发
首先构建一个拥有数组作为缓存的对象,包含 listen(订阅)和 trigger(发布)方法:
let yourMsg = {};
yourMsg.peopleList = [];
// 订阅:将函数加入队列
yourMsg.listen = function (fn) {
this.peopleList.push(fn);
}
// 发布:遍历并执行所有回调
yourMsg.trigger = function () {
for (var i = 0, fn; fn = this.peopleList[i++];) {
fn.apply(this, arguments); // 透传参数给回调
}
}
yourMsg.listen(name => console.log(name + '收到了消息'));
yourMsg.trigger('张三'); // 输出: 张三收到了消息
第二版:增加 Key 过滤机制
上述代码有个缺陷:只要有人发消息,所有人都会收到所有信息。如果想让不同的人关注不同的频道(如婚礼通知 vs. 失业八卦),就需要引入“主题”(Key)的概念:
let yourMsg = {};
yourMsg.clientList = {}; // 改用对象存储不同主题的队列
// 订阅特定主题
yourMsg.listen = function (key, fn) {
if (!this.clientList[key]) {
this.clientList[key] = [];
}
this.clientList[key].push(fn);
}
// 发布特定主题
yourMsg.trigger = function () {
let key = Array.prototype.shift.call(arguments); // 获取主题名
let fns = this.clientList[key];
// 如果该主题没人订阅,直接返回
if (!fns || fns.length === 0) return false;
// 执行该主题下的所有回调
for (var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments);
}
}
yourMsg.listen('marriage', name => console.log(name + '关心婚礼'));
yourMsg.listen('badnews', name => console.log(name + '关心坏消息'));
yourMsg.trigger('marriage', '张三'); // 仅匹配婚姻频道
yourMsg.trigger('badnews', '李四'); // 仅匹配坏消息频道
第三版:全局安装(Mixin)功能
为了复用,我们将核心逻辑提取为 event 对象,并提供一个“动态安装”函数,让任何普通对象都能获得发布订阅的能力:
var event = {
clientList: {},
listen: function (key, fn) { ... }, // 同上
trigger: function () { ... }, // 同上
remove: function (key, fn) { // 取消订阅
var fns = this.clientList[key];
if(!fns) return false;
if(!fn) { fns && (fns.length = 0); return; }
// 根据具体函数引用移除
for(let index = 0; index < fns.length; index++) {
const _fn = fns[index];
if(_fn === fn) { fns.splice(index, 1); }
}
}
};
// 混入函数
function installEvent(obj) {
for (let i in event) {
obj[i] = event[i];
}
}
// 使用示例
let yourMsg = {};
installEvent(yourMsg);
yourMsg.listen('msg', () => console.log('New Message'));
yourMsg.trigger('msg');
进阶:离线消息堆栈
通常我们遵循“先订阅再发布”的顺序,但如果发布者先发出了消息,此时尚无订阅者怎么办?类似于 QQ 的离线消息,我们可以建立一个堆栈暂存这些事件。等以后有对象来订阅时,再重新回放堆栈中的事件:
// 假设修改 event 结构增加 offline 属性
event.offline = [];
event.trigger = function () {
let key = Array.prototype.shift.call(arguments);
let fns = this.clientList[key];
if (!fns || fns.length === 0) {
// 如果没有人订阅,先将事件包装进函数存入堆栈
this.offline.push(() => {
this.trigger(key, ...arguments);
});
return false;
}
// 正常执行流程...
// ...
}
与传统观察者模式的对比
在 Java 或 C# 等强类型语言中,观察者模式通常需要显式继承接口或传入对象引用。而 JavaScript 凭借闭包和一等公民的函数对象,使得实现更加轻量:
// Java 风格的示意
interface Observer { update(event); }
class Dependent implements Observer { ... }
而在 JS 中,我们只需传递回调函数本身即可,不需要创建复杂的类层次结构。Node.js 中的 EventEmitter 和 Vue 2.x 的响应式系统底层都是基于此模式实现的。
小结
发布-订阅模式有效地实现了模块间的解耦。其优点在于逻辑清晰、扩展性强;缺点是如果滥用,可能导致内存占用增加(未释放的监听器)和调试困难(因为消息流向不明显)。 以上就是JavaScript发布订阅模式实现与原理的详细内容,更多关于JavaScript事件模型设计的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













