JavaScript设计模式:发布订阅模式详解与实现

作者:函数茶馆时间:2026-10-26 07:51:00浏览:605

本文深入解析JavaScript中的发布订阅模式(观察者模式),通过DOM事件绑定、通讯录通知等生活化案例阐述其核心原理。详细展示了从基础实现到支持Key过滤、离线消息堆栈及动态安装功能的完整代码演进,并对比了与传统订阅模式的差异,最后探讨了该模式在Vue、Node.js等现代前端架构中的解耦优势与潜在性能开销。

简介

发布-订阅模式,亦称观察者模式,定义了一种一对多的依赖关系。当目标对象的状态发生变化时,所有依赖于它的对象都会收到通知并做出相应反应。

回忆曾经

在前端开发中,为DOM节点绑定事件处理函数是一项极其常见的操作。例如,我们可以编写如下代码:

    document.body.addEventListener('click',function () {
        alert(2333);
    },false);
    document.body.click();//模拟点击事件

上述代码中,我们订阅了document.body的click事件。当body被点击时,系统会向所有订阅者发布该消息,执行回调函数弹出“2333”。我们可以自由地增加或删除订阅者,一旦消息发布,所有已注册的订阅者都会接收到通知。

    document.body.addEventListener('click',function () {
        alert(11111);
    },false);
    document.body.addEventListener('click',function () {
        alert(222);
    },false);
    document.body.addEventListener('click',function () {
        alert(333);
    },false);
    document.body.click();//模拟点击事件

需要注意的是,手动触发事件时,虽然可以直接调用document.body.click(),但更规范的做法是在IE浏览器中使用fireEvent,而在标准浏览器中使用dispatchEvent,具体实现如下:

    let fireEvent = function (element,event) {
        if (document.createEventObject) {
            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);
        }
    }
    document.addEventListener('shout',function (event) {
        alert('shout');
    })
    fireEvent(document,'shout');

畅谈现在

人际交往中充满了各种信息交换。假设你有很多朋友,当你结婚时,你可以作为发布者,遍历通讯录,逐一打电话通知所有订阅者这一消息。抽象来看,实现发布-订阅模式需要包含以下要素:

  • 发布者(即发送消息的主体)
  • 缓存列表(用于存储订阅者,如同通讯录记录了所有关注你消息的朋友)
  • 发布消息时,遍历缓存列表,依次执行其中存放的订阅者回调函数(相当于逐个打电话通知)
  • 此外,回调函数可以接收多个参数。订阅者可以获取这些参数,例如通知婚礼的时间和地点,并根据各自的需求进行后续处理。
let yourMsg = {};
yourMsg.peopleList = [];
yourMsg.listen = function (fn) {
    this.peopleList.push(fn);
}
yourMsg.triger = function () {
    for(var i = 0,fn;fn=this.peopleList[i++];){
        fn.apply(this,arguments);
    }
}
yourMsg.listen(function (name) {
    console.log(`${name}收到了你的消息`);
})
yourMsg.listen(function (name) {
    console.log('哈哈');
})
yourMsg.triger('张三');
yourMsg.triger('李四');

简介

以上是一个简单的发布-订阅实现示例。然而,我们发现订阅者会收到发布者发布的每一条信息。如果某些订阅者(如李四)只关心特定类型的消息(如负面消息),而不想接收其他无关信息,我们就需要引入Key机制,允许订阅者只订阅自己感兴趣的消息类别。

let yourMsg = {};
yourMsg.peopleList ={};
yourMsg.listen = function (key,fn) {
    if (!this.peopleList[key]) { //如果没有订阅过此类消息,创建一个缓存列表
        this.peopleList[key] = [];
    }
    this.peopleList[key].push(fn);
}
yourMsg.triger = function () {
    let key = Array.prototype.shift.call(arguments);
    let fns = this.peopleList[key];
    if (!fns || fns.length == 0) {//没有订阅 则返回
        return false;
    }
    for(var i=0,fn;fn=fns[i++];){
        fn.apply(this,arguments);
    }
}
yourMsg.listen('marrgie',function (name) {
    console.log(`${name}想知道你结婚`);
})
yourMsg.listen('unemployment',function (name) {
    console.log(`${name}想知道你失业`);
})
yourMsg.triger('marrgie','张三');
yourMsg.triger('unemployment','李四');

简介

既然每个人都需要发布和接收消息,我们有必要将发布-订阅的功能封装到一个独立对象中,以便动态安装该功能(通过installEvent函数实现)。这种设计提高了代码的复用性,具体参考前端手写面试题详细解答。

var event = {
    peopleList:[],
    listen:function (key,fn) {
        if (!this.peopleList[key]) { //如果没有订阅过此类消息,创建一个缓存列表
        this.peopleList[key] = [];
        }
        this.peopleList[key].push(fn)
    },
    trigger:function () {
         let key = Array.prototype.shift.call(arguments);
        let fns = this.peopleList[key];
        if (!fns || fns.length == 0) {//没有订阅 则返回
            return false;
        }
        for(var i=0,fn;fn=fns[i++];){
            fn.apply(this,arguments);
        }
    }
}
var installEvent  = function (obj) {
    for(var i in event){
        obj[i] = event[i];
    }
}
let yourMsg = {};
installEvent(yourMsg);
yourMsg.listen('marrgie',function (name) {
    console.log(`${name}想知道你结婚`);
})
yourMsg.listen('unemployment',function (name) {
    console.log(`${name}想知道你失业`);
})
yourMsg.trigger('marrgie','张三');
yourMsg.trigger('unemployment','李四');

有时我们需要取消订阅,例如因矛盾将好友从通讯录中移除。为此,我们需要为event对象增加一个remove方法,以支持移除特定的订阅关系。

remove:function (key,fn) {
      var fns = this.clientList[key];
      if(!fns){
          return false;
      }  
      if(!fn){
          fns && (fns.length=0)
      }else{
          for (let index = 0; index < fns.length; index++) {
              const _fn = fns[index];
              if(_fn === fn){
                  fns.splice(index,1);
              }
          }
      }
    }

发布-订阅的顺序探讨

通常我们看到的模式是先订阅再发布,但这并非绝对。如果发布者先发布消息,而此时尚无订阅者,我们可以像QQ离线消息一样,将消息保存在服务器中,待接收者下次登录时再发送。同理,我们可以建立一个存放离线事件的堆栈。当事件发布时,若无订阅者,则将发布动作封装在函数中存入堆栈;当有对象订阅该事件时,遍历堆栈并依次执行这些包装函数(即重发事件)。需要注意的是,离线事件的生命周期通常只有一次,类似于QQ未读消息提示。

JavaScript实现发布-订阅模式的便利性

得益于JavaScript回调函数的特性,实现发布-订阅模式相对简单。而在传统语言(如Java)中,通常需要将订阅者自身作为引用传入发布者对象,且订阅者需提供一个名为update的方法供发布者调用。下面代码用JS模拟这种传统实现方式。

function Dep() {
    this.subs = [];
}
Dep.prototype.addSub = function (sub) {
    this.subs.push(sub);
}
Dep.prototype.notify = function () {
    this.subs.forEach(sub=>sub.update());
}
function Watcher(fn) {
    this.fn = fn;
}
Watcher.prototype.update = function () {
     this.fn();
}
var dep = new Dep();
dep.addSub(new Watcher(function () {
    console.log('okokok');
}))
dep.notify();

小结

  • 发布-订阅模式的优势在于实现了时间上的解耦和对象之间的解耦。从架构层面看,MVC、MVVM等架构均离不开发布-订阅模式的支持。我们常用的Vue框架也是基于此模式构建的,后续将有机会深入分析Vue源码。此外,Node.js中的EventEmitter也是发布-订阅模式的典型应用,此前也已手写过其实现。
  • 然而,发布-订阅模式也存在缺点。创建订阅者会消耗一定的时间和内存资源。如果订阅了某条消息但该消息最终未发生,该订阅者仍会占用内存。若在程序中大量使用发布-订阅模式,可能会增加调试难度,使得追踪Bug变得困难。

到此这篇关于Javascript设计模式之发布订阅模式的文章就介绍到这了,更多相关JS发布订阅模式内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

作者最新文章