Javascript设计模式之发布-订阅模式

简介

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

回忆曾经

做为一名前端开发人员,给DOM节点绑定事件但是再频繁不过的事情。好比以下代码vue

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

这里咱们订阅了document.body的click事件,当body被点击的时候,他就向订阅者发布这个消息,弹出2333.咱们也能够随意的增长和删除订阅者,当消息一发布,全部的订阅者都会收到消息。node

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未读消息只会提示你一次同样。bash

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变得困难。
相关文章
相关标签/搜索