前面一篇文章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
> 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
構造函數很簡單,就一行代碼,主要邏輯都在EventEmitter.init
裏面:
EventEmitter.init
裏面也是作了一些初始化的工做,this._events
跟咱們本身寫的this.events
功能是同樣的,用來存儲訂閱的事件。核心代碼我在圖上用箭頭標出來了。這裏須要注意一點,若是一個類型的事件只有一個訂閱,this._events
就直接是那個函數了,而不是一個數組,在源碼裏面咱們會屢次看到對這個進行判斷,這樣寫是爲了提升性能。
訂閱事件
代碼傳送門: https://github.com/nodejs/node/blob/master/lib/events.js#L405
EventEmitter
訂閱事件的API是on
和addListener
,從源碼中咱們能夠看出這兩個方法是徹底同樣的:
這兩個方法都是調用了_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是removeListener
和off
,這兩個是徹底同樣的。EventEmitter
的取消訂閱API不單單會刪除對應的訂閱,在刪除後還會emit一個removeListener
事件來通知外界。這裏也會對this._events
裏面對應的type
進行判斷,若是隻有一個,也就是說這個type
的類型是function
,會直接刪除這個鍵,若是有多個訂閱,就會找出這個訂閱,而後刪掉他。若是全部訂閱都刪完了,就直接將this._events
置空:
總結
本文講解了發佈訂閱模式的原理,並本身實現了一個簡單的發佈訂閱模式。在瞭解了原理後,還去讀了Node.js的EventEmitter
模塊的源碼,進一步學習了生產環境的發佈訂閱模式的寫法。總結下來發布訂閱模式有如下特色:
- 解決了「回調地獄」
- 將多個模塊進行了解耦,本身執行時,不須要知道另外一個模塊的存在,只須要關心發佈出來的事件就行
- 由於多個模塊能夠不知道對方的存在,本身關心的事件多是一個很遙遠的旮旯發佈出來的,也不能經過代碼跳轉直接找到發佈事件的地方,debug的時候可能會有點困難。
文章的最後,感謝你花費寶貴的時間閱讀本文,若是本文給了你一點點幫助或者啓發,請不要吝嗇你的贊和GitHub小星星,你的支持是做者持續創做的動力。
「前端進階知識」系列文章及示例源碼: https://github.com/dennis-jiang/Front-End-Knowledges
歡迎關注個人公衆號進擊的大前端第一時間獲取高質量原創~