從發布訂閱模式入手讀懂Node.js的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中,這種模式可以幫助我們解開“回調地獄”。他的流程如下圖所示:

image-20200323161211669

  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

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

觀察者模式

這里再提一個很相似的設計模式:觀察者模式,有些文章認為他和發布訂閱模式是一樣的,有些認為他們是有區別的。筆者認為他更像一個低配版的發布訂閱模式,我們來實現一個看看:

class Subject {
  constructor() {
    // 一個數組存放所有的訂閱者
    // 每個消息對應一個數組,數組結構如下
    // [
    //   {
    //     observer: obj,
    //     action: () => {}
    //   }
    // ]
    this.observers = [];
  }

  addObserver(observer, action) {
    // 將觀察者和回調放入數組
    this.observers.push({observer, action});
  }

  notify(...args) {
    // 執行每個觀察者的回調
    this.observers.forEach(item => {
      const {observer, action} = item;
      action.call(observer, ...args);
    })
  }
}

const subject = new Subject();

// 添加一個觀察者
subject.addObserver({name: 'John'}, function(msg){
  console.log(this.name, 'got message: ', msg);
})

// 再添加一個觀察者
subject.addObserver({name: 'Joe'}, function(msg) {
  console.log(this.name, 'got message: ', msg);
})

// 通知所有觀察者
subject.notify('tomorrow is Sunday');

上述代碼的輸出是:

image-20200323205318223

通過這個輸出可以看出一旦調了通知的方法notify,所有觀察者都會收到通知,而且會收到同樣的信息。而發布訂閱模式還可以自定義需要接受的通知,所以說觀察者模式是低配版的發布訂閱模式。

總結

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

  1. 解決了“回調地獄”
  2. 將多個模塊進行了解耦,自己執行時,不需要知道另一個模塊的存在,只需要關心發布出來的事件就行
  3. 因為多個模塊可以不知道對方的存在,自己關心的事件可能是一個很遙遠的旮旯發布出來的,也不能通過代碼跳轉直接找到發布事件的地方,debug的時候可能會有點困難。
  4. 觀察者模式是低配版的發布訂閱模式,一旦發布通知,所有觀察者都會收到消息,不能做到發布訂閱那樣精細的控制。

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

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

“前端進階知識”系列文章:https://juejin.im/post/5e3ffc85518825494e2772fd

“前端進階知識”系列文章源碼GitHub地址: https://github.com/dennis-jiang/Front-End-Knowledges

QR1270


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM