发布-订阅发布-订阅是极其基础和重要的设计模式之一,如果你想在面试中考察一个设计模式,我想我会选择发布-毫不犹豫地订阅。那么到底什么是发布订阅,适用于哪些场景呢?刚开始学习这个模型的时候,我也是一头雾水。大佬们跟我说前端的事件绑定就是一个发布订阅(黑色问号脸)。是的,这确实是,这句话是不是总结了发布和订阅?深入学习和理解发布订阅并不容易,但也不难。人们常说,一本书读一百遍,就会明白其中的含义。很多事情只是时间问题。用多了自然就来了。下面我结合我的学习过程,说说我对发布-订阅模型的理解。写一个简单的publish-subscribepublish-subscribe当然分为订阅和发布两部分,就像关注的公众号一样,只有订阅了才会收到他的发布。订阅和发布总要有一个载体,或者说一个目标。以公众号的订阅发布为例,那么载体应该是公众号,比如我的公众号网富就是??一个载体,所有的订阅者应该都存放在公众号的某个地方。请关注我的公众号:网富,发1024获取更多前端学习资源:申报载体,将所有订阅者放入该载体。//声明一个公众号作为运营商类Account{constructor(name){//这里的名字没有特殊意义,我的本意是指代当前新的公众号namethis.name=name//所有订阅所有的订阅者都放在this的subscribers属性上公众号this.subscribers={}}}运营商已经存在,接下来就是在调度中心classAccount中添加订阅的过程{//订阅过程,名称为订阅者的账号,fn代表订阅的事件//订阅者可能会订阅多个事件,所以将事件作为一个数组//考虑到重复订阅的可能性,set也可以作为订阅事件的容器subscribe(name,fn){if(typeoffn!=='function')returnconstsubscribers=this.subscribersif(subscribers[name]){//deduplication!subscribers[name].includes(fn)&&subscribers[name].push(fn)}else{subscribers[name]=[fn]}}}接下来是发布流程classAccount{//发布流程可能只针对部分订阅者,比如某个用户发了一条消息//那么公众号只针对Reply,所以这里只发布给某个订阅者publish(name){constsubscribers=this.subscribersif(subscribers[name]){subscribers[name].forEach(fn=>fn())}}}至整个订阅发布完成。目前我们已经实现了最基本的订阅和发布功能。比如张三订阅了我的公众号,执行过程如下:constwebRuichi=newAccount('webRuichi')//张三订阅webRuichi.subscribe('张三',function(){console.log(`张三订阅了公众号`)})//公众号发布内容给张三webRuichi.publish('张三')//输出-->张三订阅了公众号所以至此,我们的订阅发布已经可以运行了,但是还存在一些不足。比如张三在订阅内容的时候需要一些特殊的信息。当公众号发布张三的信息时,把你需要的东西发给他。接下来,对发布过程进行修改:..rest))}}}接下来张三订阅的时候,可以告诉公众他需要什么样的信息://张三订阅的时候,他想知道自己订阅的是哪个公众号。webRuichi.subscribe('张三',function(name){console.log(`张三订阅了"${name}"公众号`)})//公众号告诉张三自己的公众号发布时的名字webRuichi.publish('张三',webRuichi.name)//输出-->张三已经订阅了“webRuichi”公众号订阅发布的实现很简单,就是把订阅者的事件放到在自己的数组中,在发布的时候,获取对应的订阅者,依次执行事件。应用于web前端领域,最常见的就是事件绑定。初始化时为某个按钮订阅相关事件(这里以点击事件为例),触发点击时释放。这个按钮是一个承载事件的载体,事件由订阅者订阅,并在点击事件触发时发布。大家应该都清楚,订阅发布是用来解耦的。作为一个初学者,为什么他与订阅和发布脱钩?这不是函数数组的顺序执行,解耦了吗?还有,他真正的潜力在哪里?带着这些问题继续探索。在哪里使用发布和订阅?Vue组件传参相信大家在面试中都遇到过Vue中组件间传参的问题。当组件嵌套太深时,普通的props传参会很麻烦,比如组件A要给表弟B组件传参,路径会是这样:A组件—>父组件—>爷爷组件—>叔叔组件—>B组份,前后会经过三个组份,这会对三层组份造成污染,也会给后期维护带来很多麻烦。这时候以发布订阅的方式传递参数就会非常方便。我们看一下代码://main.js文件importVuefrom'vue'importEventfrom'./event'//引入订阅并发布Vue.prototype.$event=newEvent()//一个组件//Bcomponent/template>从这个例子你可以很明显的在组件A和B组件中的订阅事件是完全独立的,A组件中发布的逻辑发生变化不会影响B组件中的逻辑。但是之前的props传参耦合了中间的三层组件。一旦要传递的参数数量改变,中间的三层组件也必须相应改变。异步回调中的订阅发布由于javascript运行机制,代码中充斥着各种异步回调。比如node中的这样一个场景:浏览器请求node服务器,node返回对应的页面,但是页面需要读取数据库和读取模板文件进行两次I/O操作。如果使用传统的回调,它会是这样的:constfs=require('fs')constqueryMysql=require('./queryMysql')fs.readFile('./template.ejs','utf8',(err,template)=>{if(err)throwerrqueryMysql('SELECT*FROMuser',(err,data)=>{if(err)throwerrrender(template,data)})})我们分析上面的代码:可读性很差,实现不够优雅。回调嵌套有两层,每一层回调都需要进行错误处理。本来可以并行的I/O变成了串行,性能上很耗时。这只是两层嵌套。如果比较异步,在回调中会很麻烦。在异步回调的开发过程中,有一种使用发布-订阅的方式来简化。然后我们使用订阅发布来改进上面的代码:constfs=require('fs')constEvent=require('event')constqueryMysql=require('./queryMysql')consteventEmitter=newEvent()//Here,订阅的事件以闭包的形式返回,目的是让html成为一个局部变量constgenReadyEvent=()=>{consthtml={}constTOTAL_KEY_COUNT=2//有两个数据用于渲染模板return(key,data)=>{html[key]=dataif(Object.keys(html).length===TOTAL_KEY_COUNT){render(html[template],html[data])}}}eventEmitter.subscribe('就绪',genReadyEvent())fs.readFile('./template.ejs','utf8',(err,template)=>{if(err)throwerreventEmitter.publish('ready','template',template)})queryMysql('SELECT*FROMuser',(err,data)=>{if(err)throwerreventEmitter.publish('ready','data',data)})上述代码改进后,第一个代码的可读性有了很大的提升,性能也得到了提升。并行I/O操作充分利用了node的特性。另外如果渲染逻辑发生变化,一般是readyEvent内部的逻辑发生变化,与订阅处的逻辑完全解耦。订阅发布的再完善通过上面的例子,介绍了订阅发布的实现和应用。实际上,目前的实现还是存在一些缺陷。例如,用户无法退订。在某些情况下,他们可能只需要订阅一次。如果订阅的事件在执行过程中被取消了某个订阅...下面我们来解决上面的问题:要取消订阅,我们需要提供一个取消订阅的方法classAccount{unsubscribe(name,fn){constsubscribers=this.subscribersif(subscribers[name]){//如果没有提供相应的事件,则删除整个订阅if(!fn){deletesubscribers[name]}elseif(typeoffn==='function'){constindex=subscribers[name].findIndex(event=>event===fn)//如果要移除的事件没有被订阅,则索引为-1(~按位非运算符)~index&&subscribers[name].splice(index,1)}}}}仅订阅一次后取消订阅的类帐户{subscribeOnce(name,fn){if(typeoffn!=='function')returnconstwrap=()=>{fn()this.unsubscribe(name,fn)}this.subscribe(name,fn)}}如果某个订阅中的某个事件在发布时取消了后续要发布的事件,遍历时可能会出现数组崩溃的问题,所以这里我们对subscribe的进行改造事件,这样你退订的时候也需要做一个转换。classAccount{constructor(name){this.name=namethis.subscribers={}}subscribe(name,fn){if(typeoffn!=='function')返回constsubscribers=this.subscribersif(subscribers[name]){//包装订阅的事件constevent={hasRemoved:false,event:fn}subscribers[name].push(event)}else{constevent={hasRemoved:false,event:fn}subscribers[name]=[event]}}//只订阅一次的代码保持不变//取消订阅的代码也需要修改unsubscribe(name,fn){constsubscribers=this.subscribersif(subscribers[name]){//Ifnot如果提供了相应的事件,则整个订阅将被删除=>eventInfo.event===fn)target&&(target.hasRemoved=true)}}}//发布的代码需要修改publish(name,...rest){constsubscribers=this.subscribersconstevents=subscribers[name]if(events){for(leti=0,len=events.length;i
