摘要:典型和改造挑戰(zhàn)了解事件發(fā)布訂閱系統(tǒng)實(shí)現(xiàn)思想,我們來(lái)看一段簡(jiǎn)單且典型的基礎(chǔ)實(shí)現(xiàn)上面代碼,實(shí)現(xiàn)了一個(gè)類我們維護(hù)一個(gè)類型的,對(duì)不同事件的所有回調(diào)函數(shù)進(jìn)行維護(hù)。方法對(duì)指定事件進(jìn)行回調(diào)函數(shù)存儲(chǔ)方法對(duì)指定的觸發(fā)事件,逐個(gè)執(zhí)行其回調(diào)函數(shù)。
新書(shū)終于截稿,今天稍有空閑,為大家奉獻(xiàn)一篇關(guān)于 JavaScript 語(yǔ)言風(fēng)格的文章,主角是函數(shù)聲明式。
靈活的 JavaScript 及其 multiparadigm相信“函數(shù)式”這個(gè)概念對(duì)于很多前端開(kāi)發(fā)者早已不再陌生:我們知道 JavaScript 是一門(mén)非常靈活,融合多模式(multiparadigm)的語(yǔ)言,這篇文章將會(huì)展示 JavaScript 里命令式語(yǔ)言風(fēng)格和聲明式風(fēng)格的切換,目的在于使讀者了解這兩種不同語(yǔ)言模式的各自特點(diǎn),進(jìn)而在日常開(kāi)發(fā)中做到合理選擇,發(fā)揮 JavaScript 的最大威力。
為了方便說(shuō)明,我們從典型的事件發(fā)布訂閱系統(tǒng)入手,一步步完成函數(shù)式風(fēng)格的改造。事件發(fā)布訂閱系統(tǒng),即所謂的觀察者模式(Pub/Sub 模式),秉承事件驅(qū)動(dòng)(event-driven)思想,實(shí)現(xiàn)了“高內(nèi)聚、低耦合”的設(shè)計(jì)。如果讀者對(duì)于此模式尚不了解,建議先閱讀我的原創(chuàng)文章:探索 Node.js 事件機(jī)制源碼 打造屬于自己的事件發(fā)布訂閱系統(tǒng)。這篇文章中從 Node.js 源碼入手,剖析了事件發(fā)布訂閱系統(tǒng)的實(shí)現(xiàn),并基于 ES Next 語(yǔ)法,實(shí)現(xiàn)了一個(gè)命令式的事件發(fā)布模式。對(duì)于此基礎(chǔ)內(nèi)容,本文不再過(guò)多展開(kāi)。
典型 EventEmitter 和改造挑戰(zhàn)了解事件發(fā)布訂閱系統(tǒng)實(shí)現(xiàn)思想,我們來(lái)看一段簡(jiǎn)單且典型的基礎(chǔ)實(shí)現(xiàn):
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](); } } } }
上面代碼,實(shí)現(xiàn)了一個(gè) EventManager 類:我們維護(hù)一個(gè) Map 類型的 eventMap,對(duì)不同事件的所有回調(diào)函數(shù)(handler)進(jìn)行維護(hù)。
addEventListener 方法對(duì)指定事件進(jìn)行回調(diào)函數(shù)存儲(chǔ);
dispatchEvent 方法對(duì)指定的觸發(fā)事件,逐個(gè)執(zhí)行其回調(diào)函數(shù)。
在消費(fèi)層面:
const em = new EventManager(); em.addEventListner("hello", function() { console.log("hi"); }); em.dispatchEvent("hello"); // hi
這些都比較好理解。下面我們的挑戰(zhàn)是:
將以上 20 多行命令式的代碼,轉(zhuǎn)換為 7 行 2 個(gè)表達(dá)式的聲明式代碼;
不再使用 {...} 和 if 判斷條件;
采用純函數(shù)實(shí)現(xiàn),規(guī)避副作用;
使用一元函數(shù),即函數(shù)方程式中只需要一個(gè)參數(shù);
使函數(shù)實(shí)現(xiàn)可組合(composable);
代碼實(shí)現(xiàn)要干凈、優(yōu)雅、低耦合。
Step1: 使用函數(shù)取代 class基于以上挑戰(zhàn)內(nèi)容,addEventListener 和 dispatchEvent,不再作為 EventManager 類的方法出現(xiàn),而成為兩個(gè)獨(dú)立的函數(shù),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 這兩個(gè)函數(shù):
export default {addEventListener, dispatchEvent};
同時(shí)使用 import 引入依賴,注意 import 使用都是單例模式(singleton):
import * as EM from "./event-manager.js"; EM.dispatchEvent("event");
因?yàn)槟K是單例情況,所以在不同文件引入時(shí),內(nèi)部變量 eventMap 是共享的,完全符合預(yù)期。
Step2: 使用箭頭函數(shù)箭頭函數(shù)區(qū)別于傳統(tǒng)的函數(shù)表達(dá)式,更符合函數(shù)式“口味”:
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](); } } }
這里要格外注意箭頭函數(shù)對(duì) this 的綁定。
Step3: 去除副作用,增加返回值為了保證純函數(shù)特性,區(qū)別于上述處理,我們不能再去改動(dòng) eventMap,而是應(yīng)該返回一個(gè)全新的 Map 類型變量,同時(shí)對(duì) addEventListener 和 dispatchEvent 方法的參數(shù)進(jìn)行改動(dòng),增加了“上一個(gè)狀態(tài)”的 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; }
沒(méi)錯(cuò),這個(gè)過(guò)程就和 Redux 中的 reducer 函數(shù)極其類似。保持函數(shù)的純凈,是函數(shù)式理念中極其重要的一點(diǎn)。
Step4: 去除聲明風(fēng)格的 for 循環(huán)接下來(lái),我們使用 forEach 代替 for 循環(huán):
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: 應(yīng)用二元運(yùn)算符
我們使用 || 和 && 來(lái)使代碼更加具有函數(shù)式風(fēng)格:
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 語(yǔ)句的表達(dá)式:
return ( eventMap.has(event) && eventMap.get(event).forEach(a => a()) ) || event;Step6: 使用三目運(yùn)算符代替 if
三目運(yùn)算符更加直觀簡(jiǎn)潔:
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: 去除花括號(hào) {...}
因?yàn)榧^函數(shù)總會(huì)返回表達(dá)式的值,我們不在需要任何 {...} :
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 化
最后一步就是實(shí)現(xiàn) currying 化操作,具體思路將我們的函數(shù)變?yōu)橐辉ㄖ唤邮芤粋€(gè)參數(shù)),實(shí)現(xiàn)方法即使用高階函數(shù)(higher-order function)。為了簡(jiǎn)化理解,讀者可以認(rèn)為即是將參數(shù) (a, b, c) 簡(jiǎn)單的變成 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;
如果讀者對(duì)于此理解有一定困難,建議先補(bǔ)充一下 currying 化知識(shí),這里不再展開(kāi)。
當(dāng)然這樣的處理,需要考慮一下參數(shù)的順序。我們通過(guò)實(shí)例,來(lái)進(jìn)行消化。
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 的讀者可能會(huì)更好理解 partial 的概念。簡(jiǎn)單來(lái)說(shuō),函數(shù)的 partial 應(yīng)用可以理解為:
函數(shù)在執(zhí)行時(shí),要帶上所有必要的參數(shù)進(jìn)行調(diào)用。但是,有時(shí)參數(shù)可以在函數(shù)被調(diào)用之前提前獲知。這種情況下,一個(gè)函數(shù)有一個(gè)或多個(gè)參數(shù)預(yù)先就能用上,以便函數(shù)能用更少的參數(shù)進(jìn)行調(diào)用。
對(duì)于 onHello 函數(shù),其參數(shù)即表示 hello 事件觸發(fā)時(shí)的回調(diào)。這里 myEventMap2 以及 hello 事件等都是預(yù)先設(shè)定好的。對(duì)于 hello 函數(shù)同理,它只需要出發(fā) 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 的讀者,如果閱讀過(guò) Redux 源碼,對(duì)于 compose 一定并不陌生。我們通過(guò) compose,實(shí)現(xiàn)了對(duì)于 hello 事件的兩個(gè)回調(diào)函數(shù)組合,以及 log 函數(shù)組合。
關(guān)于 compose 方法的奧秘,以及不同實(shí)現(xiàn)方式,請(qǐng)關(guān)注作者:Lucas HC,我將會(huì)專門(mén)寫(xiě)一篇文章介紹,并分析為什么 Redux 對(duì) compose 的實(shí)現(xiàn)稍顯晦澀,同時(shí)剖析一種更加直觀的實(shí)現(xiàn)方式。
總結(jié)函數(shù)式理念也許對(duì)于初學(xué)者并不是十分友好。讀者可以根據(jù)自身熟悉程度以及偏好,在上述 8 個(gè) steps 中,隨時(shí)停止閱讀。同時(shí)歡迎討論。
本文意譯了 Martin Novák 的 新文章,歡迎大神斧正。
廣告時(shí)間:
如果你對(duì)前端發(fā)展,尤其 React 技術(shù)棧感興趣:我的新書(shū)中,也許有你想看到的內(nèi)容。關(guān)注作者 Lucas HC,新書(shū)出版將會(huì)有送書(shū)活動(dòng)。
Happy Coding!
PS: 作者?Github倉(cāng)庫(kù)?和?知乎問(wèn)答鏈接?歡迎各種形式交流。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/94125.html
摘要:典型和改造挑戰(zhàn)了解事件發(fā)布訂閱系統(tǒng)實(shí)現(xiàn)思想,我們來(lái)看一段簡(jiǎn)單且典型的基礎(chǔ)實(shí)現(xiàn)上面代碼,實(shí)現(xiàn)了一個(gè)類我們維護(hù)一個(gè)類型的,對(duì)不同事件的所有回調(diào)函數(shù)進(jìn)行維護(hù)。方法對(duì)指定事件進(jìn)行回調(diào)函數(shù)存儲(chǔ)方法對(duì)指定的觸發(fā)事件,逐個(gè)執(zhí)行其回調(diào)函數(shù)。 showImg(https://segmentfault.com/img/remote/1460000014287200); 新書(shū)終于截稿,今天稍有空閑,為大家奉...
環(huán)境:Node v8.2.1; Npm v5.3.0;OS Windows10 1、 Node事件介紹 Node大多數(shù)核心 API 都采用慣用的異步事件驅(qū)動(dòng)架構(gòu),其中某些類型的對(duì)象(觸發(fā)器)會(huì)周期性地觸發(fā)命名事件來(lái)調(diào)用函數(shù)對(duì)象(監(jiān)聽(tīng)器)。 所有能觸發(fā)事件的對(duì)象都是 EventEmitter 類的實(shí)例。 這些對(duì)象開(kāi)放了一個(gè) eventEmitter.on() 函數(shù),允許將一個(gè)或多個(gè)函數(shù)綁定到會(huì)被對(duì)象...
摘要:從這個(gè)系列的第一章開(kāi)始到第五章,基于的響應(yīng)式編程的基礎(chǔ)知識(shí)基本上就介紹完了,當(dāng)然有很多知識(shí)點(diǎn)沒(méi)有提到,比如,等,不是他們不重要,而是礙于時(shí)間精力等原因沒(méi)辦法一一詳細(xì)介紹。 從這個(gè)系列的第一章開(kāi)始到第五章,基于rxjs的響應(yīng)式編程的基礎(chǔ)知識(shí)基本上就介紹完了,當(dāng)然有很多知識(shí)點(diǎn)沒(méi)有提到,比如 Scheduler, behaviorSubject,replaySubject等,不是他們不重要,...
摘要:使用構(gòu)造函數(shù)的原型繼承相比使用原型的原型繼承更加復(fù)雜,我們先看看使用原型的原型繼承上面的代碼很容易理解。相反的,使用構(gòu)造函數(shù)的原型繼承像下面這樣當(dāng)然,構(gòu)造函數(shù)的方式更簡(jiǎn)單。 五天之前我寫(xiě)了一個(gè)關(guān)于ES6標(biāo)準(zhǔn)中Class的文章。在里面我介紹了如何用現(xiàn)有的Javascript來(lái)模擬類并且介紹了ES6中類的用法,其實(shí)它只是一個(gè)語(yǔ)法糖。感謝Om Shakar以及Javascript Room中...
閱讀 3868·2021-07-28 18:10
閱讀 2576·2019-08-30 15:44
閱讀 1081·2019-08-30 14:07
閱讀 3454·2019-08-29 17:20
閱讀 1577·2019-08-26 18:35
閱讀 3533·2019-08-26 13:42
閱讀 1816·2019-08-26 11:58
閱讀 1585·2019-08-23 18:33