新书终于截稿,今天稍有空闲,为你们奉献一篇关于 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!