JavaScript发布订阅模式实现与原理

Elena
Elena
发布时间:2026-10-16| 5人浏览过
原创

发布订阅模式(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事件模型设计的资料请关注本站其它相关文章!

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。