摘要:典型和改造挑战了解事件发布订阅系统实现思想,我们来看一段简单且典型的基础实现上面代码,实现了一个类我们维护一个类型的,对不同事件的所有回调函数进行维护。方法对指定事件进行回调函数存储方法对指定的触发事件,逐个执行其回调函数。
新书终于截稿,今天稍有空闲,为大家奉献一篇关于 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仓库 和 知乎问答链接 欢迎各种形式交流。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/94125.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中...
阅读 3847·2021-07-28 18:10
阅读 2558·2019-08-30 15:44
阅读 1064·2019-08-30 14:07
阅读 3425·2019-08-29 17:20
阅读 1560·2019-08-26 18:35
阅读 3517·2019-08-26 13:42
阅读 1797·2019-08-26 11:58
阅读 1546·2019-08-23 18:33