JS前端设计模式之发布订阅模式详解

目录
  • 引言
    • 例子1:
    • version1:
    • version2:
  • 总结

引言

昨天我发布了一篇关于策略模式和代理模式的文章,收到的反响还不错,于是今天我们继续来学习前端中常用的设计模式之一:发布-订阅模式。

说到发布订阅模式大家应该都不陌生,它在我们的日常学习和工作中出现的频率简直不要太高,常见的有EventBus、框架里的组件间通信、鉴权业务等等......话不多说,让我们一起进入今天的学习把!!!

发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系 当一个对象的状态发生改变时,所有依赖它的订阅者都会接收到通知。发布-订阅模式在日常应用十分广泛(js中一般用事件模型来替代传统的发布订阅模式,如addEventListener)。那发布-订阅者模式有啥用呢?

例子1:

我们举个例子,小明是一个喜欢吃包子的人,于是他每天都去楼下询问有没有包子,如果运气不好今天没有包子,小明就得白跑一趟,但是啥时候有包子小明又不知道,这让他很是困扰。那如何解决这个问题呢,这个时候发布-订阅模式就派上用场了。假如老板把小明的电话记了下来,有包子就通知小明,这样小明就不会白白跑一趟了。看到这个例子你有没有觉得这种模式很眼熟,像我们的点击事件,ajax请求的error或者success事件其实都是用了这种模式,接下来我们就用代码来还原上面小明的场景

version1:

const baoziShop = {};//定义包子铺
baoziShop.listenList = [];//缓存列表 存放订阅者的回调函数
//添加订阅者
baoziShop.listen = function (fn) {
    baoziShop.listenList.push(fn)
}
//发布消息
baoziShop.trigger = function() {
    for(let i = 0, fn; fn = baoziShop.listenList[i++]) {
        fn.apply(this, arguments);
    }
}
//接下来尝试添加监听者
baoziShop.listen( function (price, baoziType) { //小明订阅消息
    console.log(`种类:${baoziType}, 价格: ${price}`)
})
baoziShop.listen( function (price, baoziType) { //小王订阅消息
    console.log(`种类:${baoziType}, 价格: ${price}`)
})
//接下来我们尝试发布消息
baoziShop.trigger(2, '豆沙包');//输出:种类:豆沙包, 价格 2
baoziShop.trigger(3, '肉包');//输出:种类:肉包,价格 3

上面我们已经实现了一个简单的例子,但是上面的代码还存在着一些问题:比如订阅者无差别接收到发布者发布的所有消息,如果小明只喜欢吃菜包,那他不应该收到上架肉包子的通知,所以我们有必要增加一个key来让订阅者只订阅自己感兴趣的东西,接下来我们对代码进行一些改动:

version2:

const baoziShop = {}; //定义包子铺
baoziShop.listenList = {}; //存放订阅者的回调函数 注意 这里从前面的数组改成了对象
//添加订阅者 key用来标识订阅者
baoziShop.listen = function(key, fn) {
    if( !this.listenList[key]) {
        this.listenList[key] = [];//如果没有订阅过此类消息 就给该消息创建订阅列表
    }
    this.listenList[key].push(fn);//将回调放入订阅列表
}
//发布消息
baoziShop.trigger = function() {
    const key = Array.prototype.shift.call(arguments), //取出消息类型
	fns = this.listenList[key];//取出该订阅对应的回调列表
    if(!fns || fns.length === 0) return false;//没有订阅则直接返回
    for(let i = 0, fn; fn = fns[i]; i++) {
        fn.apply(this, arguments) //绑定this
    }
}
//接下来我们尝试下订阅不同的消息
baoziShop.listen('菜包子', function(price) { //小明订阅菜包子的消息
    console.log('价格:', price)
})
baoziShop.listen('肉包子', function(price) { //小王订阅肉包子
    console.log('价格:', price)
})
//接下来我们发布下消息
baoziShop.trigger('菜包子', 2); //只有订阅菜包子的小明能收到消息
baoziShop.trigger('肉包子', 3); //只有订阅肉包子的小王能收到通知

好了,经过上面的改写,我们已经实现了只收到自己订阅的类型的消息的功能。那我们不妨想一下我们的代码还有啥可以完善的功能,比如如果小明楼下有两个包子铺,如果小明想要在另一个包子铺买v包子,那这段代码就必须在另一个包子铺的对象上复制粘贴一遍,如果只有两个包子铺还好,那万一有十个包子铺呢?是不是得写十遍?

所以我们正确的做法应该是将发布-订阅的功能单独抽离出来封装在一个通用的对象内,这样避免重复写同样的代码,那我们按着这种思路开始改写我们的代码

const event = {
    listenList : [], //订阅列表
    listen: function (key, fn) {
        if( !this.listenList[key]) {
        this.listenList[key] = [];//如果没有订阅过此类消息 就给该消息创建订阅列表
    }
    this.listenList[key].push(fn);//将回调放入订阅列表
    },
    trigger: function() {
        const key = Array.prototype.shift.call(arguments), //取出消息类型
	fns = this.listenList[key];//取出该订阅对应的回调列表
    if(!fns || fns.length === 0) return false;//没有订阅则直接返回
    for(let i = 0, fn; fn = fns[i]; i++) {
        fn.apply(this, arguments) //绑定this
    }
    }
}

可以看到,我们将发布-订阅那部分的逻辑抽离到event对象上,后续我们就能通过event.trigger()这种形式调用,接下来我们封装一个可以给所有对象都动态安装发布-订阅功能的方法,避免重复操作

const installEvent = function(obj) {
    for(let i in event) {
        obj[i] = event[i];
    }
}
//接下来我们测试下我们的代码
const baoziShop = {};//定义包子铺
installEvent(baoziShop);
//接下来我们就可以订阅和发布消息了
baoziShop.listen('菜包子', function(price) { //小明订阅菜包子的消息
    console.log('价格:', price)
})
baoziShop.listen('肉包子', function(price) { //小王订阅肉包子
    console.log('价格:', price)
})
baoziShop.trigger('菜包子', 2); //只有订阅菜包子的小明能收到消息
baoziShop.trigger('肉包子', 3); //只有订阅肉包子的小王能收到通知

有没有发现,经过上面的改写,我们已经可以轻松做到给每个对象都添加订阅和发布消息,再也不用重复写代码了。那趁热打铁,我们再思考一下,能否让我们的代码功能更多些,比如如果有一天,小明不想吃包子了,但是小明还是会继续收到包子铺的消息,这让他很烦恼,于是他想要取消之前在包子铺的订阅,这就引出了另一个需求,有订阅就应该有取消订阅的功能!

接下来我们开始改写我们的代码吧

//我们给我们的event对象增加一个remove的方法用来取消订阅
event.remove = function(key, fn) {
    const fns = this.listenList[key];//取出该key对应的列表
    if(!fns) { //如果该key没被人订阅,直接返回
        return false;
    } if(!fn) { //如果传入了key但是没有对应的回调函数,则标识取消该key对应的所有订阅!!
        fns && (fns.length == 0)
    }else {
        for(let len = fns.length - 1; len >= 0; len --) { //反向遍历订阅的回调列表
            const _fn = fns[len];
            if(_fn === fn) {
                fns.splice(len, 1) ;//删除订阅者的回调函数
            }
        }
    }
}
//接下来我们照常给包子铺添加一些订阅
const baoziShop = {};
installEvent(baoziShop);
baoziShop.listen('菜包子', fn1 = function(price) { //小明订阅消息
    console.log('价格', price);
})
baoziShop.listen('菜包子', fn2 = function(price) { //小王订阅消息
    console.log('价格', price)
})
baoziShop.trigger('菜包子', 2);//小明和小王都收到消息
baoziShop.remove('菜包子', fn1); //删除小明的订阅
baoziShop.trigger('菜包子', 2);//只有小王会收到订阅

至此,我们的系统已经可以添加不同的订阅,赋予对象订阅-发布功能,取消订阅等等。

理论上,我们的代码已经可以实现简单的功能,但是还存在着下面几个问题:

  • 每个对象都必须添加listentrigger的功能,以及分配一个listenList的订阅列表,这其实是资源的浪费
  • 代码的耦合度太高,就像下面这样
//小明必须知道包子铺的名称才能开始订阅
baoziShop.listen('菜包子', function(price) {
    //....
})
//如果小明要去另外的包子铺买 就必须订阅另一家包子铺
baoziAnother.listen('菜包子', function(price) {
    //....
})

这样未免有点愚蠢,我们想下现实的例子,如果我们想买包子,我们需要一家一家去和老板说吗?不需要的,我们大可以打开美团,在美团上购买就可以了,这其中,美团就类似于中介,我们只需要告诉美团我想吃包子,并不用关心包子是从哪里来的,而卖家只需要将消息发布到美团上,不用关心谁是消费者(这里和现实有点差异,因为现实我们买东西还是要看商家评价啥的,这里只是举个例子),所以我们可以改写下我们的代码

//我们尝试改写event对象 使其充当一个中介的角色 将发布者和订阅者连接起来
const Event = ({
	const listenList = {};//订阅列表
    //添加订阅者
    const listen = function(key, fn) {
    	if( !this.listenList[key]) {
        this.listenList[key] = [];//如果没有订阅过此类消息 就给该消息创建订阅列表
    }
    	this.listenList[key].push(fn);//将回调放入订阅列表
	};
	//发布消息
	const trigger = function() {
         const key = Array.prototype.shift.call(arguments), //取出消息类型
		fns = this.listenList[key];//取出该订阅对应的回调列表
    	if(!fns || fns.length === 0) return false;//没有订阅则直接返回
   	 	for(let i = 0, fn; fn = fns[i]; i++) {
       		 fn.apply(this, arguments) //绑定this
    		}
    };
	//取消订阅
	const remove = function(key, fn) {
        	const fns = this.listenList[key];//取出该key对应的列表
    if(!fns) { //如果该key没被人订阅,直接返回
        return false;
    } if(!fn) { //如果传入了key但是没有对应的回调函数,则标识取消该key对应的所有订阅!!
        fns && (fns.length == 0)
    }else {
        for(let len = fns.length - 1; len >= 0; len --) { //反向遍历订阅的回调列表
            const _fn = fns[len];
            if(_fn === fn) {
                fns.splice(len, 1) ;//删除订阅者的回调函数
            }
        }
    };
    return {
        listen,
        trigger,
        remove
    }
})();
//接下来我们就能用Event来实现发布-订阅功能而不需要创建那么多的对象了
Event.listen('菜包子', function(price) { //小明订阅消息
    console.log('价格:', price)
})
Event.listen('菜包子', 2);//包子铺发布消息

经过修改,我们现在订阅消息不再需要知道包子铺的名称,也不需要给每个包子铺都创建一个对象,只需要统一通过Event对象来订阅就好,而发布消息也是这样的流程,这样我们就巧妙地通过Event这个中介对象把发布者和订阅者联系起来了。

我们的发布订阅模式不止可用于上面这种例子,比较常见的还有模块间的通信(学过vue或者react的小伙伴应该都对组件间的事件响应不陌生),接下来就看看怎么使用

//例如我们在a元素发布一个消息 b元素就可以监听到并实施对应的操作
a.onclick = () => {
    Event.listen('onclickEvent', 'this is data')
}
//b元素接收到消息
const b = (function() {
    Event.listen('onclikcEvent', function(data) {
        console.log('这是接收到的数据', data);//输出这是接收到的数据thisisdata
    })
})();

这种用法在我们日常开发中用到的非常多!

同样,我们也可以把它用在有关登录的业务上,想象这么一个需求,如果在用户登陆后,首页需要更新用户推荐内容,用户个人信息和好友列表等,那我们应该怎么做呢?

由于我们并不知道用户啥时候会登录,所以我们可以在登录成功后发布登录成功的消息,然后在需要登录权限的地方去监听登录成功的消息并做相关操作,就像下面这样

//在登录成功后发布消息
login().then((data:[code]) => {
    if(code === 200) {
        Event.trigger('success', code);//登录成功后发布消息
    }
})
//用户信息模块监听并更新
Event.listen('success', function(code) => {
             refleshUserInfo();//更新用户信息
             })

这样,即使后面有其他模块需要鉴权,也只需要添加对应的订阅者就可以了,不用去改动登录部分的代码和逻辑,这对于代码的健壮性是有很好的帮助的。

总结

关于发布-订阅模式就讲这么多,可以看到这种设计模式还是用处非常大的,实现难度也不大,但是也要注意一些小细节,比如注意命名冲突(每个key都是唯一的,可用ES6的Symbol单独封装到专门文件),比如会消耗一定的内存和时间,因为你订阅一个消息后,除非手动取消,不然订阅者会一一直存在于内存中造成浪费等等,但是总的来说发布-订阅模式的用处和好处还是非常多的,希望大家都可以掌握并熟练使用这种模式!!

前端常见的设计模式和使用场景

一文带你读懂作用域、作用链和this的原理

更多关于JS发布订阅模式的资料请关注我们其它相关文章!

(0)

相关推荐

  • 用JS写一个发布订阅模式

    目录 1 场景引入 2 代码优化 2.1 解决增加粉丝问题 2.2 解决添加作品问题 3 观察者模式 4 经纪人登场 5 发布订阅模式 6 观察者模式和发布订阅模式的对比 什么是发布订阅模式?能手写实现一下吗?它和观察者模式有区别吗?... 1 场景引入 我们先来看这么一个场景: 假设现在有一个社交平台,平台上有一个大V叫Nami Nami很牛,多才多艺,目前她有2个技能:会写歌.会拍视频 她会把这些作品发布到平台上.关注她的粉丝就会接收到这些内容 现在他已经有3个粉丝了,分别是:Luffy.Z

  • JavaScript设计模式之观察者模式与发布订阅模式详解

    本文实例讲述了JavaScript设计模式之观察者模式与发布订阅模式.分享给大家供大家参考,具体如下: 学习了一段时间设计模式,当学到观察者模式和发布订阅模式的时候遇到了很大的问题,这两个模式有点类似,有点傻傻分不清楚,博客起因如此,开始对观察者和发布订阅开始了Google之旅.对整个学习过程做一个简单的记录. 观察者模式 当对象间存在一对多关系时,则使用观察者模式(Observer Pattern).比如,当一个对象被修改时,则会自动通知它的依赖对象.观察者模式属于行为型模式.在观察模式中共存

  • JavaScript设计模式之观察者模式(发布订阅模式)原理与实现方法示例

    本文实例讲述了JavaScript设计模式之观察者模式(发布订阅模式)原理与实现方法.分享给大家供大家参考,具体如下: 观察者模式,又称为发布订阅模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己的状态. 在观察者模式中,并不是一个对象调用另一个对象的方法,而是一个对象订阅另一个对象的特定活动并在状态改变后获得通知.订阅者也称为观察者,而被观察的对象称为发布者或主题.当发生了一个重要的事件时,发布

  • JavaScript设计模式发布订阅模式

    目录 前言 发布订阅设计模式 前言 发布订阅设计模式是和观察者设计模式基本上相同,但是他们两个设计模式不同的是发布订阅者拥有一个事件处理中心而观察者并没有 比如,我们利用订阅者设计模式去监听一个对象的改变,可以给对象改变的方法添加多个行为以及一个行为添加多个方法进行处理 发布订阅设计模式 发布订阅设计模式只需要一个类,类中拥有一个事件中心管理这行为的任务对列,我们利用这个构造函数创建一个实例,在进行模拟一个addEventListener()事件,进行触发事件中心行为上任务对列的方法 我们来举一

  • Javascript发布订阅模式介绍

    发布订阅模式介绍 发布---订阅模式又叫观察者模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知. 现实生活中的发布-订阅模式: 比如小红最近在淘宝网上看上一双鞋子,但是呢联系到卖家后,才发现这双鞋卖光了,但是小红对这双鞋又非常喜欢,所以呢联系卖家,问卖家什么时候有货,卖家告诉她,要等一个星期后才有货,卖家告诉小红,要是你喜欢的话,你可以收藏我们的店铺,等有货的时候再通知你,所以小红收藏了此店铺,但与此同时,小明,

  • js简单粗暴的发布订阅示例代码

    什么是发布/订阅? 我打个比方,你去某个门店买衣服,你和门店店长相互并不认识,门店店长只管卖他的衣服,并不关心是谁来买,而你也只管买你想要的衣服,并不关心是哪个门店在卖,这时,门店和你就组成了一个发布/订阅的关系. 当门店挂出衣服款式,你去找你想要的衣服,如果找到了,那就买下来,如果没找到,那就离开这家店.整个过程就是这么简单. 使用场景 异步通信.多页面间相互通信,pageA 的方法想在 pageB的方法调用的某个时机触发 直接上代码 class Publish { constructor()

  • JS前端设计模式之发布订阅模式详解

    目录 引言 例子1: version1: version2: 总结 引言 昨天我发布了一篇关于策略模式和代理模式的文章,收到的反响还不错,于是今天我们继续来学习前端中常用的设计模式之一:发布-订阅模式. 说到发布订阅模式大家应该都不陌生,它在我们的日常学习和工作中出现的频率简直不要太高,常见的有EventBus.框架里的组件间通信.鉴权业务等等......话不多说,让我们一起进入今天的学习把!!! 发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系 当一个对象的状态发生改变时,所有

  • vue2从数据变化到视图变化发布订阅模式详解

    目录 引言 一.发布订阅者模式的特点 二.vue中的发布订阅者模式 1.dep 2.Object.defineProperty 3.watcher 4.dep.depend 5.dep.notify 6.订阅者取消订阅 小结 引言 发布订阅者模式是最常见的模式之一,它是一种一对多的对应关系,当一个对象发生变化时会通知依赖他的对象,接受到通知的对象会根据情况执行自己的行为. 假设有财经报纸送报员financialDep,有报纸阅读爱好者a,b,c,那么a,b,c想订报纸就告诉financialDe

  • JavaScript实现与使用发布/订阅模式详解

    本文实例讲述了JavaScript实现与使用发布/订阅模式.分享给大家供大家参考,具体如下: 一.发布/订阅模式简介 发布/订阅模式(即观察者模式): 设计该模式背后的主要动力是促进形成松散耦合.在这种模式中,并不是一个对象调用另一个对象的方法,而是一个订阅者对象订阅发布者对象的特定活动,并在发布者对象的状态发生改变后,订阅者对象获得通知.订阅者也称为观察者,而被观察的对象称为发布者或主题.当发生了一个重要的事件时,发布者将会通知(调用)所有订阅者,并且可能经常以事件对象的形式传递消息. 基本思

  • JavaScript设计模式之中介者模式详解

    目录 中介者模式 现实中的中介者 中介者模式的例子 泡泡堂游戏 为游戏增加队伍 玩家增多带来的困扰 用中介者模式改造泡泡堂游戏 小结 中介者模式 在我们生活的世界中,每个人每个物体之间都会产生一些错综复杂的联系.在应用程序里也是一样,程序由大大小小的单一对象组成,所有这些对象都按照某种关系和规则来通信. 平时我们大概能记住 10 个朋友的电话.30 家餐馆的位置.在程序里,也许一个对象会和其他 10 个对象打交道,所以它会保持 10 个对象的引用.当程序的规模增大,对象会越来越多,它们之间的关系

  • JavaScript设计模式之职责链模式详解

    目录 职责链模式 1. 现实中的职责链模式 2. 实际开发中的职责链模式 3. 用职责链模式重构代码 4. 灵活可拆分的职责链节点 5. 异步的职责链 6. 职责链模式的优缺点 7. 用 AOP 实现职责链 8. 小结 职责链模式 职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止. 职责链模式的名字非常形象,一系列可能会处理请求的对象被连接成一条链,请求在这些对象之间依次传递,直到遇

  • JavaScript 设计模式中的代理模式详解

    前言: 代理模式,代理(proxy)是一个对象,它可以用来控制对另一个对象的访问. 现在页面上有一个香港回归最想听的金典曲目列表: <ul id="container"> <li>我的中国心</li> <li>东方之珠</li> <li>香港别来无恙</li> <li>偏偏喜欢你</li> <li>相亲相爱</li> </ul> 需要给页面添加

  • Java设计模式中的门面模式详解

    目录 门面模式 概述 应用场景 目的 优缺点 主要角色 门面模式的基本使用 创建子系统角色 创建外观角色 客户端调用 门面模式实现商城下单 库存系统 支付系统 物流系统 入口系统 客户端调用 门面模式 概述 门面模式(Facade Pattern)又叫外观模式,属于结构性模式. 它提供一个统一的接口去访问多个子系统的多个不同的接口,它为子系统中的一组接口提供一个统一的高层接口.使得子系统更容易使用. 客户端不需要知道系统内部的复杂联系,只需定义系统的入口.即在客户端和复杂系统之间再加一层,这一层

  • Java设计模式之抽象工厂模式详解

    一.什么是抽象工厂模式 为创建一组相关或相互依赖的对象提供一个接口,而且无需指定他们的具体类,这称之为抽象工厂模式(Abstract Factory).我们并不关心零件的具体实现,而是只关心接口(API).我们仅使用该接口(API)将零件组装称为产品. 二.示例程序   1.抽象的零件:Item类 package com.as.module.abstractfactory; /** * 抽象的零件 * @author Andy * @date 2021/4/29 23:16 */ public

  • Java设计模式之职责链模式详解

    目录 前言 一.职责链模式的定义与特点 二.职责链模式的结构 三.职责链模式案例 前言 本文简单介绍了设计模式的一种--职责链模式  一.职责链模式的定义与特点 定义: 为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链:当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止. 比如我们的审批制度,低等级的审批不了的,交给上一级审批,依次类推,直到审批结束. 在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处

随机推荐