EventEmitter:コマンドJavaScript classから宣言関数式への華麗なターン


新刊書はついに原稿を切り、今日は少し暇で、JavaScript言語スタイルに関する文章をお届けします.主役は関数宣言式です.
柔軟なJavaScriptとそのmultiparadigm
「関数式」という概念は多くの先端開発者にとってすでによく知られていないと信じています.JavaScriptは非常に柔軟で、マルチモード(multiparadigm)を融合させた言語であることを知っています.この文章はJavaScriptのコマンド式言語スタイルと声明式スタイルの切り替えを示し、読者にこの2つの異なる言語モデルのそれぞれの特徴を理解させることを目的としています.さらに日常開発において合理的な選択を行い,JavaScriptの最大の威力を発揮する.
説明を容易にするために,典型的なイベントパブリケーションサブスクリプションシステムから着手し,関数式スタイルの改造を一歩一歩完成した.イベント配信サブスクリプションシステム、いわゆるオブザーバーモード(Pub/subモード)は、イベント駆動(event-driven)の考え方を受け継ぎ、「高集約、低結合」の設計を実現した.もし読者がこのモードについてまだ理解していないならば、先に私のオリジナルの文章を読むことを提案します:Nodeを探求します.jsイベントメカニズムソースコードは、独自のイベントパブリケーションサブスクリプションシステムを構築します.この文章はノードからjsソースコードを入手し,イベント配信サブスクリプションシステムの実装を解析し,ESNext構文に基づいてコマンド式のイベント配信モードを実現した.この基本的な内容については、この文書はあまり展開されません.
典型的な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メソッドは、指定されたトリガイベントに対して、そのコールバック関数を1つずつ実行する.

  • 消費レベル:
    const em = new EventManager();
    em.addEventListner('hello', function() {
      console.log('hi');
    });
    em.dispatchEvent('hello'); // hi
    

    これらはよく理解できます.次の課題は、
  • 以上の20行以上の命令式のコードを、7行2式の宣言式コードに変換する.
  • は使用されなくなりました{...}とif判断条件;
  • は純関数を採用して実現し、副作用を回避する.
  • は、関数方程式に1つのパラメータしか必要としない一元関数を使用する.
  • は、関数を組み合わせ可能にする.
  • コードは、清潔で優雅で低結合を実現します.

  • Step 1:classの代わりに関数を使う
    以上の課題に基づいて、addEventListenerとdispatchEventは、EventManagerクラスのメソッドとして現れず、2つの独立した関数となり、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という2つの関数を使用できます.
    export default {addEventListener, dispatchEvent};
    

    同時にimportを使用して依存を導入し、importの使用はすべて単例モード(singleton)であることに注意します.
    import * as EM from './event-manager.js';
    EM.dispatchEvent('event');
    

    モジュールは一例の場合であるため,異なるファイル導入時に内部変数eventMapが共有され,予想通りである.
    Step 2:矢印関数を使う
    矢印関数は、従来の関数式とは異なり、関数式の「味」に合致します.
    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へのバインドに特に注意してください.
    Step 3:副作用を取り除き、戻り値を増やす
    純粋な関数特性を保証するために、上記の処理とは異なり、eventMapを変更するのではなく、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関数と極めて類似している.関数の純粋さを保つことは、関数式の理念の中で極めて重要な点である.
    Step 4:宣言スタイルのforループを取り除く
    次に、forループの代わりにforEachを使用します.
    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;
    }
    
    

    Step 5:二元演算子の適用
    コードをより関数的なスタイルにするには、|&&を使用します.
    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;
    
    
    

    Step 6:ifの代わりに三目演算子を使用する
    3つの演算子は、より直感的で簡潔です.
    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;
    }
    
    

    Step 7:カッコを外す{...}
    矢印関数は式の値を返すので、{...}は必要ありません.
    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;
    
    

    Step 8:currying化完了
    最後のステップはcurrying化操作を実現し,具体的な構想は我々の関数を1元(1つのパラメータしか受け入れられない)に変え,実現方法は高次関数(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応用は以下のように理解できる.
    関数は、実行時に必要なすべてのパラメータを持って呼び出されます.ただし、パラメータは、関数が呼び出される前に事前に知ることができる場合があります.この場合、1つの関数に1つ以上のパラメータが予め使用され、関数がより少ないパラメータで呼び出されるようにします.
    onHello関数の場合、そのパラメータはhelloイベントがトリガーされたときのコールバックを表します.ここでmyEventMap 2やハローイベントなどは予め設定されています.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イベントに対する2つのコールバック関数の組合せとlog関数の組合せを実現した.
    composeメソッドの奥義と異なる実装方法については、著者:Lucas HCに注目してください.私は専門的に文章を書いて紹介し、なぜReduxがcomposeの実装に少し難解なのかを分析し、同時により直感的な実装方法を分析します.
    まとめ
    関数式のコンセプトは初心者にはあまり友好的ではないかもしれません.読者は、自分の熟知度や好みに応じて、上記8つのstepsの中で、いつでも読書を止めることができます.同時に討論を歓迎します.
    本文はMartin Novákの新しい文章を意訳して、大神斧正を歓迎します.
    広告時間:フロントエンドの発展、特にReactテクノロジースタックに興味がある場合は、私の新刊書には、あなたが見たい内容があるかもしれません.著者のLucas HCに注目し、新刊書の出版には本を送る活動がある.
    Happy Coding!
    PS:著者Github倉庫と知乎問答リンクは様々な形式の交流を歓迎します.