EventEmitter:從命令式 JavaScript class 到聲明函數式的華麗轉身

從命令式到函數式

新書終於截稿,今天稍有空閒,爲你們奉獻一篇關於 JavaScript 語言風格的文章,主角是函數聲明式。javascript

靈活的 JavaScript 及其 multiparadigm

相信「函數式」這個概念對於不少前端開發者早已再也不陌生:咱們知道 JavaScript 是一門很是靈活,融合多模式(multiparadigm)的語言,這篇文章將會展現 JavaScript 裏命令式語言風格和聲明式風格的切換,目的在於使讀者瞭解這兩種不一樣語言模式的各自特色,進而在平常開發中作到合理選擇,發揮 JavaScript 的最大威力。前端

爲了方便說明,咱們從典型的事件發佈訂閱系統入手,一步步完成函數式風格的改造。事件發佈訂閱系統,即所謂的觀察者模式(Pub/Sub 模式),秉承事件驅動(event-driven)思想,實現了「高內聚、低耦合」的設計。若是讀者對於此模式尚不瞭解,建議先閱讀個人原創文章:探索 Node.js 事件機制源碼 打造屬於本身的事件發佈訂閱系統。這篇文章中從 Node.js 源碼入手,剖析了事件發佈訂閱系統的實現,並基於 ES Next 語法,實現了一個命令式的事件發佈模式。對於此基礎內容,本文再也不過多展開。java

典型 EventEmitter 和改造挑戰

瞭解事件發佈訂閱系統實現思想,咱們來看一段簡單且典型的基礎實現:python

class EventManager {
  construct (eventMap = new Map()) {
    this.eventMap = eventMap;
  }
  addEventListener (event, handler) {
    if (this.eventMap.has(event)) {
      this.eventMap.set(event, this.eventMap.get(event).concat([handler]));
    } else {
      this.eventMap.set(event, [handler]);
    }
  }
  dispatchEvent (event) {
    if (this.eventMap.has(event)) {
      const handlers = this.eventMap.get(event);
      for (const i in handlers) {
        handlers[i]();
      }
    }
  }
}

上面代碼,實現了一個 EventManager 類:咱們維護一個 Map 類型的 eventMap,對不一樣事件的全部回調函數(handler)進行維護。git

  • addEventListener 方法對指定事件進行回調函數存儲;
  • dispatchEvent 方法對指定的觸發事件,逐個執行其回調函數。

在消費層面:github

const em = new EventManager();
em.addEventListner('hello', function() {
  console.log('hi');
});
em.dispatchEvent('hello'); // hi

這些都比較好理解。下面咱們的挑戰是:數組

  • 將以上 20 多行命令式的代碼,轉換爲 7 行 2 個表達式的聲明式代碼;
  • 再也不使用 {...} 和 if 判斷條件;
  • 採用純函數實現,規避反作用;
  • 使用一元函數,即函數方程式中只須要一個參數;
  • 使函數實現可組合(composable);
  • 代碼實現要乾淨、優雅、低耦合。

Step1: 使用函數取代 class

基於以上挑戰內容,addEventListener 和 dispatchEvent,再也不做爲 EventManager 類的方法出現,而成爲兩個獨立的函數,eventMap 做爲變量:app

const eventMap = new Map();

function addEventListener (event, handler) {
  if (eventMap.has(event)) {
    eventMap.set(event, eventMap.get(event).concat([handler]));
  } else {
    eventMap.set(event, [handler]);
  }
}
function dispatchEvent (event) {
  if (eventMap.has(event)) {
    const handlers = this.eventMap.get(event);
    for (const i in handlers) {
      handlers[i]();
    }
  }
}

在模塊化的需求下,咱們能夠 export 這兩個函數:模塊化

export default {addEventListener, dispatchEvent};

同時使用 import 引入依賴,注意 import 使用都是單例模式(singleton):函數

import * as EM from './event-manager.js';
EM.dispatchEvent('event');

由於模塊是單例狀況,因此在不一樣文件引入時,內部變量 eventMap 是共享的,徹底符合預期。

Step2: 使用箭頭函數

箭頭函數區別於傳統的函數表達式,更符合函數式「口味」:

const eventMap = new Map();
const addEventListener = (event, handler) => {
  if (eventMap.has(event)) {
    eventMap.set(event, eventMap.get(event).concat([handler]));
  } else {
    eventMap.set(event, [handler]);
  }
}
const dispatchEvent = event => {
  if (eventMap.has(event)) {
    const handlers = eventMap.get(event);
    for (const i in handlers) {
      handlers[i]();
    }
  }
}

這裏要格外注意箭頭函數對 this 的綁定。

Step3: 去除反作用,增長返回值

爲了保證純函數特性,區別於上述處理,咱們不能再去改動 eventMap,而是應該返回一個全新的 Map 類型變量,同時對 addEventListener 和 dispatchEvent 方法的參數進行改動,增長了「上一個狀態」的 eventMap,以便推演出全新的 eventMap:

const addEventListener = (event, handler, eventMap) => {
  if (eventMap.has(event)) {
    return new Map(eventMap).set(event, eventMap.get(event).concat([handler]));
  } else {
    return new Map(eventMap).set(event, [handler]);
  }
}
const dispatchEvent = (event, eventMap) => {
  if (eventMap.has(event)) {
    const handlers = eventMap.get(event);
    for (const i in handlers) {
      handlers[i]();
    }
  }
  return eventMap;
}

沒錯,這個過程就和 Redux 中的 reducer 函數極其相似。保持函數的純淨,是函數式理念中極其重要的一點。

Step4: 去除聲明風格的 for 循環

接下來,咱們使用 forEach 代替 for 循環:

const addEventListener = (event, handler, eventMap) => {
  if (eventMap.has(event)) {
    return new Map(eventMap).set(event, eventMap.get(event).concat([handler]));
  } else {
    return new Map(eventMap).set(event, [handler]);
  }
}
const dispatchEvent = (event, eventMap) => {
  if (eventMap.has(event)) {
    eventMap.get(event).forEach(a => a());
  }
  return eventMap;
}

Step5: 應用二元運算符

咱們使用 || 和 && 來使代碼更加具備函數式風格:

const addEventListener = (event, handler, eventMap) => {
  if (eventMap.has(event)) {
    return new Map(eventMap).set(event, eventMap.get(event).concat([handler]));
  } else {
    return new Map(eventMap).set(event, [handler]);
  }
}
const dispatchEvent = (event, eventMap) => {
  return (
    eventMap.has(event) &&
    eventMap.get(event).forEach(a => a())
  ) || event;
}

須要格外注意 return 語句的表達式:

return (
    eventMap.has(event) &&
    eventMap.get(event).forEach(a => a())
  ) || event;

Step6: 使用三目運算符代替 if

三目運算符更加直觀簡潔:

const addEventListener = (event, handler, eventMap) => {
  return eventMap.has(event) ?
    new Map(eventMap).set(event, eventMap.get(event).concat([handler])) :
    new Map(eventMap).set(event, [handler]);
}
const dispatchEvent = (event, eventMap) => {
  return (
    eventMap.has(event) &&
    eventMap.get(event).forEach(a => a())
  ) || event;
}

Step7: 去除花括號 {...}

由於箭頭函數總會返回表達式的值,咱們不在須要任何 {...} :

const addEventListener = (event, handler, eventMap) =>
   eventMap.has(event) ?
     new Map(eventMap).set(event, eventMap.get(event).concat([handler])) :
     new Map(eventMap).set(event, [handler]);
     
const dispatchEvent = (event, eventMap) =>
  (eventMap.has(event) && eventMap.get(event).forEach(a => a())) || event;

Step8: 完成 currying 化

最後一步就是實現 currying 化操做,具體思路將咱們的函數變爲一元(只接受一個參數),實現方法即便用高階函數(higher-order function)。爲了簡化理解,讀者能夠認爲便是將參數 (a, b, c) 簡單的變成 a => b => c 方式:

const addEventListener = handler => event => eventMap =>
   eventMap.has(event) ?
     new Map(eventMap).set(event, eventMap.get(event).concat([handler])) :
     new Map(eventMap).set(event, [handler]);
     
const dispatchEvent = event => eventMap =>
  (eventMap.has(event) && eventMap.get(event).forEach (a => a())) || event;

若是讀者對於此理解有必定困難,建議先補充一下 currying 化知識,這裏再也不展開。

固然這樣的處理,須要考慮一下參數的順序。咱們經過實例,來進行消化。

currying 化使用:

const log = x => console.log (x) || x;
const myEventMap1 = addEventListener(() => log('hi'))('hello')(new Map());
dispatchEvent('hello')(myEventMap1); // hi

partial 使用:

const log = x => console.log (x) || x;
let myEventMap2 = new Map();
const onHello = handler => myEventMap2 = addEventListener(handler)('hello')(myEventMap2);
const hello = () => dispatchEvent('hello')(myEventMap2);

onHello(() => log('hi'));
hello(); // hi

熟悉 python 的讀者可能會更好理解 partial 的概念。簡單來講,函數的 partial 應用能夠理解爲:

函數在執行時,要帶上全部必要的參數進行調用。可是,有時參數能夠在函數被調用以前提早獲知。這種狀況下,一個函數有一個或多個參數預先就能用上,以便函數能用更少的參數進行調用。

對於 onHello 函數,其參數即表示 hello 事件觸發時的回調。這裏 myEventMap2 以及 hello 事件等都是預先設定好的。對於 hello 函數同理,它只須要出發 hello 事件便可。

組合使用:

const log = x => console.log (x) || x;
const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)));
const addEventListeners = compose(
  log,
  addEventListener(() => log('hey'))('hello'),
  addEventListener(() => log('hi'))('hello')
);

const myEventMap3 = addEventListeners(new Map()); // myEventMap3
dispatchEvent('hello')(myEventMap3); // hi hey

這裏須要格外注意 compose 方法。熟悉 Redux 的讀者,若是閱讀過 Redux 源碼,對於 compose 必定並不陌生。咱們經過 compose,實現了對於 hello 事件的兩個回調函數組合,以及 log 函數組合。

關於 compose 方法的奧祕,以及不一樣實現方式,請關注做者:Lucas HC,我將會專門寫一篇文章介紹,並分析爲何 Redux 對 compose 的實現稍顯晦澀,同時剖析一種更加直觀的實現方式。

總結

函數式理念也許對於初學者並非十分友好。讀者能夠根據自身熟悉程度以及偏好,在上述 8 個 steps 中,隨時中止閱讀。同時歡迎討論。

本文意譯了 Martin Novák 的 新文章,歡迎大神斧正。

廣告時間:
若是你對前端發展,尤爲 React 技術棧感興趣:個人新書中,也許有你想看到的內容。關注做者 Lucas HC,新書出版將會有送書活動。

Happy Coding!

PS: 做者 Github倉庫 和 知乎問答連接 歡迎各類形式交流。

相關文章
相關標籤/搜索