從發佈訂閱模式入手讀懂 Node.js 的 EventEmitter 源碼

前面一篇文章setTimeout和setImmediate到底誰先執行,本文讓你完全理解Event Loop詳細講解了瀏覽器和Node.js的異步API及其底層原理Event Loop。本文會講一下不用原生API怎麼達到異步的效果,也就是發佈訂閱模式。發佈訂閱模式在面試中也是高頻考點,本文會本身實現一個發佈訂閱模式,弄懂了他的原理後,咱們就能夠去讀Node.js的EventEmitter源碼,這也是一個典型的發佈訂閱模式。javascript

本文全部例子已經上傳到GitHub,同一個repo下面還有我全部博文和例子:前端

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

爲何要用發佈訂閱模式

在沒有Promise以前,咱們使用異步API的時候常常會使用回調,可是若是有幾個互相依賴的異步API調用,回調層級太多可能就會陷入「回調地獄」。下面代碼演示了假如咱們有三個網絡請求,第二個必須等第一個結束才能發出,第三個必須等第二個結束才能發起,若是咱們使用回調就會變成這樣:node

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運行的。這個例子裏面有三層回調,咱們已經有點暈了,若是再多幾層,那真的就是「地獄」了。git

發佈訂閱模式

發佈訂閱模式是一種設計模式,並不單單用於JS中,這種模式能夠幫助咱們解開「回調地獄」。他的流程以下圖所示:github

image-20200323161211669

> 1. 消息中心:負責存儲消息與訂閱者的對應關係,有消息觸發時,負責通知訂閱者 > 2. 訂閱者:去消息中心訂閱本身感興趣的消息 > 3. 發佈者:知足條件時,經過消息中心發佈消息面試

有了這種模式,前面處理幾個相互依賴的異步API就不用陷入"回調地獄"了,只須要讓後面的訂閱前面的成功消息,前面的成功後發佈消息就好了。ajax

本身實現一個發佈訂閱模式

知道了原理,咱們本身來實現一個發佈訂閱模式,此次咱們使用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

image-20200323170909507

構造函數很簡單,就一行代碼,主要邏輯都在EventEmitter.init裏面:

image-20200323171123339

EventEmitter.init裏面也是作了一些初始化的工做,this._events跟咱們本身寫的this.events功能是同樣的,用來存儲訂閱的事件。核心代碼我在圖上用箭頭標出來了。這裏須要注意一點,若是一個類型的事件只有一個訂閱,this._events就直接是那個函數了,而不是一個數組,在源碼裏面咱們會屢次看到對這個進行判斷,這樣寫是爲了提升性能。

訂閱事件

代碼傳送門: https://github.com/nodejs/node/blob/master/lib/events.js#L405

EventEmitter訂閱事件的API是onaddListener,從源碼中咱們能夠看出這兩個方法是徹底同樣的:

image-20200323171656342

這兩個方法都是調用了_addListener,這個方法對參數進行了判斷和錯誤處理,核心代碼仍然是往this._events裏面添加事件:

image-20200323172045655

發佈事件

代碼傳送門:https://github.com/nodejs/node/blob/master/lib/events.js#L263

EventEmitter發佈事件的API是emit,這個API裏面會對"error"類型的事件進行特殊處理,也就是拋出錯誤:

image-20200323172657760

若是不是錯誤類型的事件,就把訂閱的回調事件拿出來執行:

image-20200323172822170

取消訂閱

代碼傳送門: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置空:

image-20200323174111868

總結

本文講解了發佈訂閱模式的原理,並本身實現了一個簡單的發佈訂閱模式。在瞭解了原理後,還去讀了Node.js的EventEmitter模塊的源碼,進一步學習了生產環境的發佈訂閱模式的寫法。總結下來發布訂閱模式有如下特色:

  1. 解決了「回調地獄」
  2. 將多個模塊進行了解耦,本身執行時,不須要知道另外一個模塊的存在,只須要關心發佈出來的事件就行
  3. 由於多個模塊能夠不知道對方的存在,本身關心的事件多是一個很遙遠的旮旯發佈出來的,也不能經過代碼跳轉直接找到發佈事件的地方,debug的時候可能會有點困難。

文章的最後,感謝你花費寶貴的時間閱讀本文,若是本文給了你一點點幫助或者啓發,請不要吝嗇你的贊和GitHub小星星,你的支持是做者持續創做的動力。

「前端進階知識」系列文章及示例源碼: https://github.com/dennis-jiang/Front-End-Knowledges

歡迎關注個人公衆號進擊的大前端第一時間獲取高質量原創~

QR1270

相關文章
相關標籤/搜索