新書終於截稿,今天稍有空閒,爲你們奉獻一篇關於 JavaScript 語言風格的文章,主角是函數聲明式開發。 咱們對一個簡易的,面向對象的 EventEmitter 系統,一步步改造爲函數式風格。並結合實例來講明函數式的優秀特性。javascript
相信「函數式」這個概念對於不少前端開發者早已再也不陌生:咱們知道 JavaScript 是一門很是靈活,融合多模式(multiparadigm)的語言,這篇文章將會展現 JavaScript 裏命令式語言風格和聲明式風格的切換,目的在於使讀者瞭解這兩種不一樣語言模式的各自特色,進而在平常開發中作到合理選擇,發揮 JavaScript 的最大威力。前端
爲了方便說明,咱們從典型的事件發佈訂閱系統入手,一步步完成函數式風格的改造。事件發佈訂閱系統,即所謂的觀察者模式(Pub/Sub 模式),秉承事件驅動(event-driven)思想,實現了「高內聚、低耦合」的設計。java
若是讀者對於此模式尚不瞭解,建議先閱讀個人原創文章:探索 Node.js 事件機制源碼 打造屬於本身的事件發佈訂閱系統。這篇文章中從 Node.js 事件模塊源碼入手,剖析了事件發佈訂閱系統的實現,並基於 ES Next 語法,實現了一個命令式、面向對象的事件發佈和響應器。對於此基礎內容,本文再也不過多展開。python
瞭解事件發佈訂閱系統實現思想的基礎上,咱們來看一段簡單且典型的基礎實現:git
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,對不一樣事件的全部回調函數(handlers)進行維護。github
在消費層面:數組
const em = new EventManager();
em.addEventListner('hello', function() {
console.log('hi');
});
em.dispatchEvent('hello'); // hi
複製代碼
這些都比較好理解。下面咱們的挑戰是:app
咱們先看一下最終結果對比圖:模塊化
立刻咱們就一步步介紹這種蛻變過程。函數
基於以上挑戰內容,addEventListener 和 dispatchEvent 再也不做爲 EventManager 類的方法出現,而成爲兩個獨立的函數,eventMap 做爲變量:
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 是共享的,徹底符合預期。
箭頭函數區別於傳統的函數表達式,更符合函數式「口味」:
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 的綁定。固然,箭頭函數自己也叫作 lambda 函數,從名字上就很「函數式」。
爲了保證純函數特性,區別於上述處理,咱們不能再去改動 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 函數極其相似。保持函數的純淨,是函數式理念中極其重要的一點。
接下來,咱們使用 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;
}
複製代碼
咱們使用 || 和 && 來使代碼更加簡潔直觀:
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;
複製代碼
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;
}
複製代碼
由於箭頭函數總會返回表達式的值,咱們再也不須要任何 {...} :
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;
複製代碼
最後一步就是實現 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 應用能夠理解爲:
函數在執行時,要帶上全部必要的參數進行調用。可是,有時參數能夠在函數被調用以前提早獲知。這種狀況下,一個函數有一個或多個參數預先就能用上,以便函數能用更少的參數進行調用。
好比:
const sum = a => b => a + b;
const sumTen = sum(10)
sumTen(20)
// 30
複製代碼
就是一種體現。
回到咱們的場景中,對於 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(f, g, h) 等同於 (...args) => f(g(h(...args))).
複製代碼
關於 compose 方法的奧祕,以及不一樣實現方式,請關注做者:Lucas HC,我將會專門寫一篇文章介紹,並分析爲何 Redux 對 compose 的實現稍顯晦澀,同時剖析一種更加直觀的實現方式。
函數式理念也許對於初學者並非十分友好。讀者能夠根據自身熟悉程度以及偏好,在上述 8 個 steps 中,隨時中止閱讀。同時歡迎討論。
本文意譯了 Martin Novák 的 新文章,歡迎大神斧正。
就像 @顏海鏡 大佬說的:
函數式的結果就是,到最後本身也就看不懂了。。。
廣告時間: 若是你對前端發展,尤爲 React 技術棧感興趣:個人新書中,也許有你想看到的內容。關注做者 Lucas HC,新書出版將會有送書活動。
Happy Coding!