摘要:典型和改造挑戰了解事件發布訂閱系統實現思想,我們來看一段簡單且典型的基礎實現上面代碼,實現了一個類我們維護一個類型的,對不同事件的所有回調函數進行維護。方法對指定事件進行回調函數存儲方法對指定的觸發事件,逐個執行其回調函數。
新書終于截稿,今天稍有空閑,為大家奉獻一篇關于 JavaScript 語言風格的文章,主角是函數聲明式。
靈活的 JavaScript 及其 multiparadigm相信“函數式”這個概念對于很多前端開發者早已不再陌生:我們知道 JavaScript 是一門非常靈活,融合多模式(multiparadigm)的語言,這篇文章將會展示 JavaScript 里命令式語言風格和聲明式風格的切換,目的在于使讀者了解這兩種不同語言模式的各自特點,進而在日常開發中做到合理選擇,發揮 JavaScript 的最大威力。
為了方便說明,我們從典型的事件發布訂閱系統入手,一步步完成函數式風格的改造。事件發布訂閱系統,即所謂的觀察者模式(Pub/Sub 模式),秉承事件驅動(event-driven)思想,實現了“高內聚、低耦合”的設計。如果讀者對于此模式尚不了解,建議先閱讀我的原創文章:探索 Node.js 事件機制源碼 打造屬于自己的事件發布訂閱系統。這篇文章中從 Node.js 源碼入手,剖析了事件發布訂閱系統的實現,并基于 ES Next 語法,實現了一個命令式的事件發布模式。對于此基礎內容,本文不再過多展開。
典型 EventEmitter 和改造挑戰了解事件發布訂閱系統實現思想,我們來看一段簡單且典型的基礎實現:
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)進行維護。
addEventListener 方法對指定事件進行回調函數存儲;
dispatchEvent 方法對指定的觸發事件,逐個執行其回調函數。
在消費層面:
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 作為變量:
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倉庫?和?知乎問答鏈接?歡迎各種形式交流。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/52175.html
摘要:典型和改造挑戰了解事件發布訂閱系統實現思想,我們來看一段簡單且典型的基礎實現上面代碼,實現了一個類我們維護一個類型的,對不同事件的所有回調函數進行維護。方法對指定事件進行回調函數存儲方法對指定的觸發事件,逐個執行其回調函數。 showImg(https://segmentfault.com/img/remote/1460000014287200); 新書終于截稿,今天稍有空閑,為大家奉...
環境:Node v8.2.1; Npm v5.3.0;OS Windows10 1、 Node事件介紹 Node大多數核心 API 都采用慣用的異步事件驅動架構,其中某些類型的對象(觸發器)會周期性地觸發命名事件來調用函數對象(監聽器)。 所有能觸發事件的對象都是 EventEmitter 類的實例。 這些對象開放了一個 eventEmitter.on() 函數,允許將一個或多個函數綁定到會被對象...
摘要:從這個系列的第一章開始到第五章,基于的響應式編程的基礎知識基本上就介紹完了,當然有很多知識點沒有提到,比如,等,不是他們不重要,而是礙于時間精力等原因沒辦法一一詳細介紹。 從這個系列的第一章開始到第五章,基于rxjs的響應式編程的基礎知識基本上就介紹完了,當然有很多知識點沒有提到,比如 Scheduler, behaviorSubject,replaySubject等,不是他們不重要,...
摘要:使用構造函數的原型繼承相比使用原型的原型繼承更加復雜,我們先看看使用原型的原型繼承上面的代碼很容易理解。相反的,使用構造函數的原型繼承像下面這樣當然,構造函數的方式更簡單。 五天之前我寫了一個關于ES6標準中Class的文章。在里面我介紹了如何用現有的Javascript來模擬類并且介紹了ES6中類的用法,其實它只是一個語法糖。感謝Om Shakar以及Javascript Room中...
閱讀 1662·2021-09-26 09:55
閱讀 5278·2021-09-22 15:40
閱讀 2022·2019-08-30 15:53
閱讀 1505·2019-08-30 11:15
閱讀 1723·2019-08-29 15:41
閱讀 1878·2019-08-28 18:13
閱讀 3154·2019-08-26 12:00
閱讀 1678·2019-08-26 10:30