乐趣区

关于设计模式:从发布订阅模式入手读懂Nodejs的EventEmitter源码

后面一篇文章 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 胜利的音讯,而后发动申请 2
pubSub.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 胜利的音讯,而后发动申请 3
pubSub.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

退出移动版