后面一篇文章setTimeout和setImmediate到底谁先执行,本文让你彻底了解Event Loop具体解说了浏览器和Node.js的异步API及其底层原理Event Loop。本文会讲一下不必原生API怎么达到异步的成果,也就是公布订阅模式。公布订阅模式在面试中也是高频考点,本文会本人实现一个公布订阅模式,弄懂了他的原理后,咱们就能够去读Node.js的EventEmitter源码,这也是一个典型的公布订阅模式。

本文所有例子曾经上传到GitHub,同一个repo上面还有我所有博文和例子:

https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/DesignPatterns/PubSub

为什么要用公布订阅模式

在没有Promise之前,咱们应用异步API的时候常常会应用回调,然而如果有几个相互依赖的异步API调用,回调层级太多可能就会陷入“回调天堂”。上面代码演示了如果咱们有三个网络申请,第二个必须等第一个完结能力收回,第三个必须等第二个完结能力发动,如果咱们应用回调就会变成这样:

const request = require("request");request('https://www.baidu.com', function (error, response) {  if (!error && response.statusCode == 200) {    console.log('get times 1');    request('https://www.baidu.com', function(error, response) {      if (!error && response.statusCode == 200) {        console.log('get times 2');        request('https://www.baidu.com', function(error, response) {          if (!error && response.statusCode == 200) {            console.log('get times 3');          }        })      }    })  }});

因为浏览器端ajax会有跨域问题,上述例子我是用Node.js运行的。这个例子外面有三层回调,咱们曾经有点晕了,如果再多几层,那真的就是“天堂”了。

公布订阅模式

公布订阅模式是一种设计模式,并不仅仅用于JS中,这种模式能够帮忙咱们解开“回调天堂”。他的流程如下图所示:

  1. 音讯核心:负责存储音讯与订阅者的对应关系,有音讯触发时,负责告诉订阅者
  2. 订阅者:去音讯核心订阅本人感兴趣的音讯
  3. 发布者:满足条件时,通过音讯核心公布音讯

有了这种模式,后面解决几个相互依赖的异步API就不必陷入"回调天堂"了,只须要让前面的订阅后面的胜利音讯,后面的胜利后公布音讯就行了。

本人实现一个公布订阅模式

晓得了原理,咱们本人来实现一个公布订阅模式,这次咱们应用ES6的class来实现,如果你对JS的面向对象或者ES6的class还不相熟,请看这篇文章:

class PubSub {  constructor() {    // 一个对象寄存所有的音讯订阅    // 每个音讯对应一个数组,数组构造如下    // {    //   "event1": [cb1, cb2]    // }    this.events = {}  }  subscribe(event, callback) {    if(this.events[event]) {      // 如果有人订阅过了,这个键曾经存在,就往里面加就好了      this.events[event].push(callback);    } else {      // 没人订阅过,就建一个数组,回调放进去      this.events[event] = [callback]    }  }  publish(event, ...args) {    // 取出所有订阅者的回调执行    const subscribedEvents = this.events[event];    if(subscribedEvents && subscribedEvents.length) {      subscribedEvents.forEach(callback => {        callback.call(this, ...args);      });    }  }  unsubscribe(event, callback) {    // 删除某个订阅,保留其余订阅    const subscribedEvents = this.events[event];    if(subscribedEvents && subscribedEvents.length) {      this.events[event] = this.events[event].filter(cb => cb !== callback)    }  }}

解决回调天堂

有了咱们本人的PubSub,咱们就能够用它来解决后面的回调天堂问题了:

const request = require("request");const pubSub = new PubSub();request('https://www.baidu.com', function (error, response) {  if (!error && response.statusCode == 200) {    console.log('get times 1');    // 公布申请1胜利音讯    pubSub.publish('request1Success');  }});// 订阅申请1胜利的音讯,而后发动申请2pubSub.subscribe('request1Success', () => {  request('https://www.baidu.com', function (error, response) {    if (!error && response.statusCode == 200) {      console.log('get times 2');      // 公布申请2胜利音讯      pubSub.publish('request2Success');    }  });})// 订阅申请2胜利的音讯,而后发动申请3pubSub.subscribe('request2Success', () => {  request('https://www.baidu.com', function (error, response) {    if (!error && response.statusCode == 200) {      console.log('get times 3');      // 公布申请3胜利音讯      pubSub.publish('request3Success');    }  });})

Node.js的EventEmitter

Node.js的EventEmitter思维跟咱们后面的例子是一样的,不过他有更多的错误处理和更多的API,源码在GitHub上都有:https://github.com/nodejs/node/blob/master/lib/events.js。咱们挑几个API看一下:

构造函数

代码传送门: https://github.com/nodejs/node/blob/master/lib/events.js#L64

构造函数很简略,就一行代码,次要逻辑都在EventEmitter.init外面:

EventEmitter.init外面也是做了一些初始化的工作,this._events跟咱们本人写的this.events性能是一样的,用来存储订阅的事件。外围代码我在图上用箭头标出来了。这里须要留神一点,如果一个类型的事件只有一个订阅,this._events就间接是那个函数了,而不是一个数组,在源码外面咱们会屡次看到对这个进行判断,这样写是为了进步性能。

订阅事件

代码传送门: https://github.com/nodejs/node/blob/master/lib/events.js#L405

EventEmitter订阅事件的API是onaddListener,从源码中咱们能够看出这两个办法是齐全一样的:

这两个办法都是调用了_addListener,这个办法对参数进行了判断和错误处理,外围代码依然是往this._events外面增加事件:

公布事件

代码传送门:https://github.com/nodejs/node/blob/master/lib/events.js#L263

EventEmitter公布事件的API是emit,这个API外面会对"error"类型的事件进行非凡解决,也就是抛出谬误:

如果不是谬误类型的事件,就把订阅的回调事件拿进去执行:

勾销订阅

代码传送门:https://github.com/nodejs/node/blob/master/lib/events.js#L450

EventEmitter外面勾销订阅的API是removeListeneroff,这两个是齐全一样的。EventEmitter的勾销订阅API不仅仅会删除对应的订阅,在删除后还会emit一个removeListener事件来告诉外界。这里也会对this._events外面对应的type进行判断,如果只有一个,也就是说这个type的类型是function,会间接删除这个键,如果有多个订阅,就会找出这个订阅,而后删掉他。如果所有订阅都删完了,就间接将this._events置空:

总结

本文解说了公布订阅模式的原理,并本人实现了一个简略的公布订阅模式。在理解了原理后,还去读了Node.js的EventEmitter模块的源码,进一步学习了生产环境的公布订阅模式的写法。总结下来公布订阅模式有以下特点:

  1. 解决了“回调天堂”
  2. 将多个模块进行理解耦,本人执行时,不须要晓得另一个模块的存在,只须要关怀公布进去的事件就行
  3. 因为多个模块能够不晓得对方的存在,本人关怀的事件可能是一个很边远的旮旯公布进去的,也不能通过代码跳转间接找到公布事件的中央,debug的时候可能会有点艰难。

文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和GitHub小星星,你的反对是作者继续创作的能源。

作者博文GitHub我的项目地址: https://github.com/dennis-jiang/Front-End-Knowledges