JavaScriptの[無効]からライブラリを書く


導入


国家管理と反応性を扱う良いライブラリとフレームワークがたくさんあります.シンプルで短いユーティリティなどからS.js 重い解決策のようなSolid .
任意のlib/フレームワークについては、賢明に選択することが重要です.私が選ぶのを助けるために、私が少なくとも部分的にそのソースコードを理解することができるならば、私はライブラリ/フレームワークだけを使うことに決めました.私が初心者であるので、私は理解しなければならない何かにコードを「refactor」しようとするときでも簡単に迷子になります.
私は良いライトライブラリを見つけたので™ 私にとって、そのようなシステムを実現できる方法を学ぶために、私はそれを自分で構築することを決めました.(車輪を再発明することはクールです)
私は午後にそれを書いて、それをテストするために1週かかりました.使えますValup , 値の更新には短いです.それは少し、軽量、簡単、魔法のナンセンス、プロジェクトです.
私は、私のような人々がプロセスを理解するのを助けるために、それに関する記事を作るのが楽しいと思いました.
では、どうやってやるのですか?次の手順を実行します.
  • イベントエミッタシステムの実装
  • データラッパーの実装
  • ラッパを生成する
  • 質的改善を加える
  • 各ステップでは、私のコードの簡略化された(そして、コメントされた)バージョンを示す前に、インタフェースと何を実装するべきか説明します.私のバージョンを見る前にインターフェイスを試して、実装するのは自由です.

    1 )イベントエミッタの実装


    何が';イベントエミッタ';ですか?特定の「イベント」が発火されるとき、「イベントリスナー」と呼ばれている機能を起こすシステムです.DOMでは、すでにそのようなイベント(onclick、onblurなど)に使用されるかもしれません.
    イベントエミッタは、次のインターフェイスまたはバリアントを実装する必要があります.
    interface EventEmitter {
      public events: Map<string, Array<Function>>;
      // List of events with associated listeners.
      // Example: {"myevent": [f1, f2, f3]}
    
      public addListener: (event: string, listener: Function) => void;
      // Add an entry in the 'events' map at 'event' key.
    
      public removeListener: (event: string, listener: Function) => void;
      // Remove the entry from 'events' at 'event' if the exact function exists in that list.
    
      public emit: (event: string) => void;
      // Fires all listeners to 'event'.
      // Example: for {"myevent": [f1, f2, f3]},
      // 'emit("myevent")' will execute () => {f1();f2();f3();}
    }
    
    私は、Github Gistからインスピレーションを得ましたA very simple EventEmitter in pure Javascript .
    私のコードの簡略化されて、コメントされたバージョンは、ここにあります.
    class EventEmitter {
      _events = new Map();
      // Custom getter to force the property to be a map and not anything else
      get events() {
        if (!(this._events instanceof Map)) {
          this._events = new Map();
        }
        return this._events;
      }
    
      // Same as 'addEventListener', I just prefer the
      // "myObject.on('myEvent', () => {...})" syntax.
      on(event, listener) {
        let currentEvent = this.events.get(event);
        // If the event doesn't exist, create it.
        if (typeof currentEvent !== "object") {
          currentEvent = [];
        }
        currentEvent.push(listener);
        this.events.set(event, currentEvent);
      }
    
      removeListener(event, listener) {
        // Index of listener in the array if it exists, else -1.
        const index = this.events.get(event)?.indexOf(listener) ?? -1;
        if (index <= -1) return; // Guard clause, exit immediately
    
        const currentEvent = this.events.get(event);
        currentEvent.splice(index, 1); // Remove listener from array
        this.events.set(event, currentEvent);
      }
    
      emit(event, ...args) {
        // The "...args" is for listeners arguments,
        // Example:
        //   const logger = (a) => console.log(a);
        //   myObject.on('log', logger);
        //   myObject.emit('log', 'Hello'); // logs 'Hello'
    
        const currentEvent = this.events.get(event);
        if (typeof currentEvent !== "object") return; // Guard clause
    
        // Fire all listeners with arguments.
        currentEvent.forEach((listener) => {
          listener.apply(this, args);
        });
      }
    }
    

    2 )データラッパーの実装


    我々の反応システムは魔法を使用していないので、我々はどのようなデータを使用するには、どのようなイベントを聞くには、どのように物事を更新する指示する必要があります.簡単に言うことはできないmyObject = {foo: "bar"} そして、イベントリスナーを実装しない何かでそれを取り替えたので、「MyObject」の上でイベントリスナーを発射することを期待します.
    したがって、我々は、我々が制御することができるデータを読み書きするシステムを書く必要があります.以下に例を示します.
    interface DataWrapper {
      private _data: any;
      public get data();
      public set data(newData: any);
    }
    
    // Or with a generic type
    interface DataWrapper<T> {
      private _data: T;
      public get data();
      public set data(newData: T);
    }
    
    // Or with functions instead of accessors
    interface DataWrapper<T> {
      private _data: T;
      public getData: () => T;
      public setData: (newData: T) => void;
    }
    
    私は最初のシステムを適用し、ラッパーを呼び出すことを選んだR , これは'反応'の略です.これはどこでも、したがって、単一の文字名と呼ばれるベースオブジェクトです.
    class R {
      _val = undefined;
      get val() { return this._val }
      set val(nval) { this._val = nval }
    
      constructor(newValue) { this._val = newValue }
    }
    
    
    // Usage:
    // const myObject = new R({foo: "bar"});
    // console.log(myObject.val); // '{"foo":"bar"}'
    // myObject.val = {foo: 'baz'}; // updated
    

    3 )ラッパーを生成する


    我々のデータアクセサーはイベントを放出して、受け取る必要があります.それをする一つの方法は、ラッパーを作ることですextend EventEmitator、私が選んだ方法は、それを私のラッパーの特性としてカプセル化することです.私たちはfaçade EventEmitatorを使用するメソッド(同じ名前のデザインパターンから).
    前回のインターフェイスの拡張方法の例を示します.
    interface DataWrapper {
      //...
      private _eventEmitter: EventEmitter;
      public addListener: (event: string, listener: Function) => void;
      public removeListener: (event: string, listener: Function) => void;
    
      // Custom notifier, use the 'emit' function with
      // any arguments you'd like.
      public notify: (event: string, state: any) => void;
    }
    
    以下に、どのように実装しました.
    class R {
      //...
      _eventEmitter = new EventEmitter();
    
      // addListener
      on(event, listener) {
        this._eventEmitter.on(event, listener);
      }
    
      removeListener(event, listener) {
        this._eventEmitter.removeListener(event, listener);
      }
    
      notify(event, state) {
        this._eventEmitter.emit(event, { state });
        // I chose to wrap the state in a 'state' property.
        // First to ensure that we emit an object, but also because I
        // want to make sure I, myself, won't forget that a listener 
        // is used specifically for my library.
      }
    }
    
    我々は今、作業システムを持っている!それはあまりではないが、我々はすでにそれを使用することができます.
    const username = new R('ADEA');
    
    const greeter = ({state}) => {
      console.log(`Hello, ${state.name}!`)
    };
    
    username.on('greet', greeter);
    
    username.notify('greet', {name: "ADEA"});
    // "Hello, ADEA!";
    
    私が認めるけれども、「反応している側」は全く不足しています.我々は、値が自動的にリスナーを火災に変更を知っている方法を持っていない.
    しかし、アクセラレータの使用のおかげで実際には実装が非常に簡単です.我々は単に火を放つイベントを定義しなければならないchanging 値が変更される前にchanged 更新が完了したらnotify それら.
    class R {
      //...
      set val(newValue) {
        const oldState = {
          prev: this._val,
          current: this._val,
          next: newValue
        };
        const newState = {...oldState, current: newValue};
    
        this.notify("changing", oldState);
        this._val = newValue;
        this.notify("changed", newState);
      }
    }
    
    今、我々は反応的な方法でそれを使うことができます:
    const reactiveUsername = new R('Alice');
    const greeter = ({state}) => console.log(`Hello, ${state.current}!`);
    const goodbye = ({state}) => console.log(`Goodbye, ${state.current}`.);
    
    reactiveUsername.on('changing', goodbye);
    reactiveUsername.on('changed', greeter);
    
    reactiveUsername.val = 'Bob';
    // "Goodbye, Alice."
    // "Hello, Bob!"
    
    あなたはそれを再生することができます変更/変更に関連するカスタムイベントを使用して、DOM値を積極的に変更するようにしてください.
    これまでに書いたものから最後のコードを示します.
    class EventEmitter {
      _events = new Map();
      get events() {
        if (!(this._events instanceof Map)) {
          this._events = new Map();
        }
        return this._events;
      }
    
      on(event, listener) {
        let currentEvent = this.events.get(event);
        if (typeof currentEvent !== "object") currentEvent = [];
        currentEvent.push(listener);
        this.events.set(event, currentEvent);
      }
    
      removeListener(event, listener) {
        const index = this.events.get(event)?.indexOf(listener) ?? -1;
        if (index <= -1) return;
        const currentEvent = this.events.get(event);
        currentEvent.splice(index, 1);
        this.events.set(event, currentEvent);
      }
    
      emit(event, ...args) {
        const currentEvent = this.events.get(event);
        if (typeof currentEvent !== "object") return;
        currentEvent.forEach((listener) => {
          listener.apply(this, args);
        });
      }
    }
    
    class R {
      _eventEmitter = new EventEmitter();
      _val = undefined;
      get val() { return this._val }
      set val(newValue) {
        const oldState = {
          prev: this._val,
          current: this._val,
          next: newValue
        };
        const newState = {...oldState, current: newValue};
    
        this.notify("changing", oldState);
        this._val = newValue;
        this.notify("changed", newState);
      }
      constructor(value) { this._val = value }
    
      on(event, listener) {
        this._eventEmitter.on(event, listener);
      }
      removeListener(event, listener) {
        this._eventEmitter.removeListener(event, listener);
      }
      notify(event, state) {
        this._eventEmitter.emit(event, { state });
      }
    }
    
    我々は正常にコードの100行未満で反応システムを実装している!そして、コードは“悪い”ではありません.もちろん、スペースやコメントを入れたり、別のファイルのクラスを別々にしたり、いくつかの可変長の改良を書いたりしなければなりません.

    4)質的改善の追加


    この部分はあなたです!ライブラリのスケルトンを首尾よく実装したので、お好みに合わせて変更できます.
    私はちょうどそれらを実装する方法を説明せずにいくつかの改善のアイデアを列挙します.それらのいくつかは、私のソースコードで、typescriptで書きました.
    // Improvement 1:
    // Have a strict value checking and non-strict one
    // (nonstrict emits at every update, strict emits only when differs)
    
    const strictVal = new R(false, {strict: true});
    const nonstrictVal = new R(false, {strict: false});
    strictVal.val = false; // doesn't update
    strictVal.val = true; // updates
    strictVal.val = true; // doesn't update
    nonstrictVal.val = false; // updates
    nonstrictVal.val = false; // updates
    
    // Improvement 2:
    // Use factory methods to create reactive values.
    const r1 = R.data(0);
    const r2 = R.strictData(0);
    
    // Improvement 3:
    // Allow for method chaining.
    const counterChain1 = R.data(0)
      .on('changing', () => console.log('changing'))
      .on('changed', () => console.log('changed'))
      .on('increment', ({state}) => { counterChain1.val = state.current + 1 })
      .on('decrement', ({state}) => { counterChain1.val = state.current - 1 })
    
    // Improvement 4: (not in Valup (yet?))
    // Clean an event from all listeners
    counterChain1.clean('increment')
    counterChain1.cleanAll()
    counterChain1.val = 1; // nothing happens
    
    Valup's source code (gitlab)
    Valup's doc page (gitlab pages)
    Valup's NPM entry