写点什么

面试官:请实现 Javascript 发布 - 订阅模式

  • 2022 年 10 月 06 日
    浙江
  • 本文字数:3370 字

    阅读完需:约 11 分钟

简介

发布-订阅模式又叫做观察者模式,他定义了一种一对多的依赖关系,即当一个对象的状态发生改变的时候,所有依赖他的对象都会得到通知。

回忆曾经

作为一名前端开发人员,给 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');
复制代码

畅谈现在

人的日常生活离不开各种人际交涉,比如你的朋友有很多,这时候你要结婚了,要以你为发布者,打开你的通讯录,挨个打电话通知各个订阅者你要结婚的消息。抽象一下,实现发布-订阅模式需要:


  1. 发布者(你)

  2. 缓存列表(通讯录,你的朋友们相当于订阅了你的所有消息)

  3. 发布消息的时候遍历缓存列表,依次触发里面存放的订阅者的回调函数(挨个打电话)

  4. 另外,回调函数中还可以添加很多参数,,订阅者可以接收这些参数,比如你会告诉他们婚礼时间,地点等,订阅者收到消息后可以进行各自的处理。


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 中的 EventEmitter 也是发布订阅的,之前也手写过它的实现。

  • 发布-订阅同时也是有缺点存在的,创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息以后,可能此消息最后都未发生,但是这个订阅者会始终存在于内存中。如果程序中大量使用发布-订阅的话,也会使得程序跟踪 bug 变得困难。

用户头像

还未添加个人签名 2022.07.31 加入

还未添加个人简介

评论

发布
暂无评论
面试官:请实现Javascript发布-订阅模式_JavaScript_helloworld1024fd_InfoQ写作社区