電子冒険:エピソード40:ES 6プロキシでイベントバスAPI


我々のコンポーネントは、イベントバスを介して通信し、それは我々が望むすべてを行い、実装は非常に簡単です.
一方、イベントコールは乱雑に見えます.例えば、ファイルをダブルクリックするためのハンドラです.
  function ondoubleclick() {
    eventBus.emit("app", "activatePanel", panelId)
    eventBus.emit(panelId, "focusOn", idx)
    eventBus.emit(panelId, "activateItem")
  }
なぜ、それはこのように見えませんか?
  function ondoubleclick() {
    app.activatePanel(panelId)
    panel.focusOn(idx)
    panel.activateItem()
  }
そうしましょう!
ProxyRubyのような言語では、以下のように実装するのが非常に簡単ですmethod_missing . JavaScriptは残念ながらそのようなことはありません.または少なくとも、それは使用しなかった.
ES 6作成Proxy , これは特別な種類のオブジェクトですmethod_missing と他のメタプログラミング.名前はかなり愚かです、それは良いAPIを作成するなどのプロキシのもの以外の多くの用途があるので.
ほとんどの人々は、それがES 6だけであるので、それのことを聞いたことがなく、そして、ES 6の残りとは異なり、これはBabelと交替することは不可能です.あなたがIEを支持しなければならなかった限り(バベル譲りを通して)、彼らを使う方法が全くありませんでした.
最近では、彼らは実際にVueのようなシーンの背後にあるいくつかのフレームワークによって使用されているが、彼らが作成されている厄介な方法のため、いくつかの人々が直接アプリでそれらを使用します.
また、彼らのパフォーマンスは驚くべきではありません、しかし、我々はちょうどここで良いAPIをしようとしています.

オリジナルEventBus 実装
出発点はこちら
export default class EventBus {
  constructor() {
    this.callbacks = {}
  }

  handle(target, map) {
    this.callbacks[target] = { ...(this.callbacks[target] || {}), ...map }
  }

  emit(target, event, ...details) {
    let handlers = this.callbacks[target]
    if (handlers) {
      if (handlers[event]) {
        handlers[event](...details)
      } else if (handlers["*"]) {
        handlers["*"](event, ...details)
      }
    }
  }
}
Proxy 実装
我々は欲しいeventBus.target("app") or eventBus.target(panelId) 何かを返すには、通常の関数呼び出しで使用できます.最初の部分は非常に簡単ですEventTarget オブジェクト渡しbus and target 引数として:
export default class EventBus {
  constructor() {
    this.callbacks = {}
  }

  handle(target, map) {
    this.callbacks[target] = { ...(this.callbacks[target] || {}), ...map }
  }

  emit(target, event, ...details) {
    let handlers = this.callbacks[target]
    if (handlers) {
      if (handlers[event]) {
        handlers[event](...details)
      } else if (handlers["*"]) {
        handlers["*"](event, ...details)
      }
    }
  }

  target(t) {
    return new EventTarget(this, t)
  }
}
今、我々は基本的に1つの大きな偽のオブジェクトを偽にする必要がありますmethod_missing . どちらのメソッドを呼び出しても、そのイベントを呼び出す関数が返されます.
class EventTarget {
  constructor(bus, target) {
    this.bus = bus
    this.target = target
    return new Proxy(this, {
      get: (receiver, name) => {
        return (...args) => {
          bus.emit(target, name, ...args)
        }
      }
    })
  }
}
ここでアンパックをするのがたくさんある.最初我々セットthis.bus and this.target 我々が厳密に話すとしても、彼らが閉鎖範囲にいるので、必要でありません.このようなプロキシを使ってコードをデバッグする必要があれば、コンソールでのデバッグ出力を読みやすくします.
それから値をconstructor . 値を返すconstructor ? あなたがちょうど他のどの言語にも慣れているならば、あなたは彼らのほとんどがそれを支持しないので、混乱しているかもしれません.しかし、クラスのコンストラクタは、クラスの新たなインスタンスよりも何か別のものを返すことができます.まあ、他のこともオブジェクトである限り、何らかの理由で文字列や数字を返すことはできません.
これはどうにか有効なJavaScriptです.
class Cat {
  constructor() {
    return {cat: "No Cat For You!"}
  }
  meow() {
    console.log("MEOW!")
  }
}
let cat = new Cat() // what we returned here is not a Cat
cat.meow() // TypeError: cat.meow is not a function
我々はこの機能の1つの良いユースケースを持ってProxy 我々が作成するときEventTarget . 私たちも、元のラップされていないオブジェクトを渡すthis . しかし、本当に我々は何のためにそれを使用しないでください、我々がこれまでに使うすべては、このオブジェクトですget .
これは
eventBus.target("app").activatePanel(panelId)
以下に変換します.
(new EventTarget(eventBus, "app")).activatePanel(panelId)
そうすると、
(new Proxy(eventTarget, {get: ourGetFunction})).activatePanel(panelId)
以下のようになります.
proxy.get("activatePanel")(panelId)
以下のようになります.
((...args) => { eventBus.emit("app", name, ...args) })(panelId)
最後に
eventBus.emit("app", name, panelId)

これの使い方?
実装は舞台裏で複雑でした、しかし、我々にはより良いAPIがあります:
  let app = eventBus.target("app")
  let panel = eventBus.target(panelId)

  function onclick() {
    app.activatePanel(panelId)
    panel.focusOn(idx)
  }
  function onrightclick() {
    app.activatePanel(panelId)
    panel.focusOn(idx)
    panel.flipSelected(idx)
  }
  function ondoubleclick() {
    app.activatePanel(panelId)
    panel.focusOn(idx)
    panel.activateItem()
  }
これは、
  function onclick() {
    eventBus.emit("app", "activatePanel", panelId)
    eventBus.emit(panelId, "focusOn", idx)
  }
  function onrightclick() {
    eventBus.emit("app", "activatePanel", panelId)
    eventBus.emit(panelId, "focusOn", idx)
    eventBus.emit(panelId, "flipSelected", idx)
  }
  function ondoubleclick() {
    eventBus.emit("app", "activatePanel", panelId)
    eventBus.emit(panelId, "focusOn", idx)
    eventBus.emit(panelId, "activateItem")
  }

より多くのプロキシ?
代わりに2層目のプロキシを使うことができました.
let app = eventBus.target("app")
let panel = eventBus.target(panelId)
それから、
let app = eventBus.app
let panel = eventBus[panelId]
それをするために、私たちはProxy からEventBus コンストラクタget 呼び出しthis.target . 読者のための運動としてこれを任せます.

なぜ我々はこれが必要ですか?
明らかな質問は:なぜ我々はこれが必要ですか?
なぜ、我々はちょうど代わりにこれをすることができませんかApp.svelte ):
  eventBus.app = {switchPanel, activatePanel, quit, openPalette, closePalette}
  eventBus.activePanel = eventBus[$activePanel]
そうすると、次のようにコードで使えます:
  let app = eventBus.app
  let panel = eventBus[panelId]
  let activePanel = eventBus.activePanel

  app.switchPanel(panelId)
これには2つの問題があります.最初のコンポーネントはいくつかの順序で作成されます.したがって、1つのコンポーネントが初期化されたときにこれを実行したい場合、他のコンポーネントはまだそのイベントをeventBus.something かもしれないundefined その時点で.これはいくつかの遅延コールバックや反応性で回避することができますが、それはいくつかの他のボイラー板を保存するboilerplateを追加しています.
より大きな問題はlet activePanel = eventBus.activePanel . 我々がそうするならば、それはセットされますactivePanel このコードが実行されたときにいずれかのパネルがアクティブになっていることを示します.それで、我々はそれを反応させる必要があるでしょう、しかし、何に関して?
これを行うならば
$ activePanel = eventBus[$activePanelId]
次に、すべてのコンポーネントはアクティブパネルのIDでいくつかのストアにアクセスする必要があります.だから、さらに多くのboilerplate以上.EventBus ベースの解決策は、イベントが実際にトリガされたときにターゲットを検索するだけで、そのような問題はありません.

結果
これまでの結果と同じです.

次のエピソードでは、おそらく聞いたことのないフレームワークを試してみましょう.
いつものように.all the code for the episode is here .