美文网首页前端开发那些事让前端飞Web前端之路
EventEmitter:从命令式 JavaScript cla

EventEmitter:从命令式 JavaScript cla

作者: LucasHC | 来源:发表于2018-04-10 15:15 被阅读95次
    从命令式到函数式

    新书终于截稿,今天稍有空闲,为大家奉献一篇关于 JavaScript 语言风格的文章,主角是函数声明式开发。
    我们对一个简易的,面向对象的 EventEmitter 系统,一步步改造为函数式风格。并结合实例来说明函数式的优秀特性。

    灵活的 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,对不同事件的所有回调函数(handlers)进行维护。

    • 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 的绑定。当然,箭头函数本身也叫做 lambda 函数,从名字上就很“函数式”。

    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

    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 应用可以理解为:

    函数在执行时,要带上所有必要的参数进行调用。但是,有时参数可以在函数被调用之前提前获知。这种情况下,一个函数有一个或多个参数预先就能用上,以便函数能用更少的参数进行调用。

    比如:

    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!

    PS: 作者 Github仓库知乎问答链接 欢迎各种形式交流。

    相关文章

      网友评论

      • 四爷在此:到最后自己确实看不懂了,代码可读性下降,不利于团队维护。问个问题,是否在不需要维护内部数据的时候,用函数式更好,写需要维护复杂状态(内部数据)的组件,还是应该采用class 模式?
        LucasHC:@四爷在此 这是个极端的例子 我觉得正常情况下还是 class 吧,更人性化
      • 微醺岁月:大佬,么么哒,好久没见:kissing_heart::kissing_heart::kissing_heart:
        微醺岁月:@LucasHC 年薪没一百万不敢上知乎:stuck_out_tongue_winking_eye:
        LucasHC:@微醺岁月 你咋用简书这么多 知乎不上
      • 颜海镜:嗯,函数式的结果就是,到最后就看不懂了 哈哈
        LucasHC:@颜海镜 so true!

      本文标题:EventEmitter:从命令式 JavaScript cla

      本文链接:https://www.haomeiwen.com/subject/wrqnhftx.html