🐷

宣言的なJSフレームワーク 配列プロパティの展開をProxyに変えてみる

2022/06/12に公開約2,800字

概要

フレームワークで行っていた、viewModelの配列プロパティの展開をProxyに変えてみた。

フレームワーク

v0.6.0以降

https://github.com/mogera551/data-x.js/

今までの仕組み

viewModel"aaa.*"の配列プロパティは、"aaa.1""aaa.2"のような要素プロパティに展開し、viewModelへ作成していた。配列が変更されると、要素プロパティを展開しなおしていた。

JavaScript
class AppViewModel {
  "@fruits" = [ "バナナ", "りんご", "オレンジ" ];
  "@@fruits.*";
  /* 実行時、下記のように展開し、要素プロパティを作成する */
  "@@fruits.0"; // バナナ
  "@@fruits.1"; // りんご
  "@@fruits.2"; // オレンジ
}
viewModel["fruits.0"]; // バナナ
viewModel["fruits.1"]; // りんご
viewModel["fruits.2"]; // オレンジ

欠点

  • 配列の更新を検知し要素プロパティを展開しなおす必要がある。
  • 多階層の配列プロパティを要素プロパティに展開するロジックが複雑で、バグの温床になりつつ...

解決策

  • 要素プロパティを展開するのではなく、Proxyで要素プロパティのアクセスを実現する
  • Proxysetに更新通知を組み込んで集約すれば一石二鳥

https://zenn.dev/mogera/articles/36406bad25fc4a

Proxyで要素プロパティ

要素プロパティ"fruits.0"にアクセスする場合、AppViewModelの配列プロパティ"fruits.*"にマッチするか検証し、マッチする場合インデックスを保存し、配列プロパティ"fruits.*"にアクセスを行う。setでは、更新通知を送信する。

JavaScript
class AppViewModel {
  "@fruits" = [ "バナナ", "りんご", "オレンジ" ];
  "@@fruits.*";
}

class ProxyHandler {
  #context;
  get(target, prop, receiver) {
    // プロパティ
    if (prop in target) return Reflect.get(target, prop, receiver);
    // fruits.0などAppViewModelにないプロパティ
    // fruits.*にマッチするか
    const result = /^fruits\.([0-9a-zA-Z_]+)$/.exec(prop);
    if (result) {
      // インデックスを保存し、"fruits.*"プロパティにアクセス
      const indexes = result.slice(1);
      return this.#context.pushIndexes(indexes, () => {
        return Reflect.get(target, "fruits.*", receiver);
      });
    }
  }
  set(target, prop, value, receiver) {
    // プロパティ
    if (prop in target) {
      return Reflect.set(target, prop, value, receiver);
      // 更新を通知
      this.#context.notifier.notify({ name:prop });
    }
    // fruits.0などAppViewModelにないプロパティ
    // fruits.*にマッチするか
    const result = /^fruits\.([0-9a-zA-Z_]+)$/.exec(prop);
    if (result) {
      // インデックスを保存し、"fruits.*"プロパティにセット
      const indexes = result.slice(1);
      return this.#context.pushIndexes(indexes, () => {
        Reflect.set(target, "fruits.*", value, receiver);
	// 更新を通知
	this.#context.notifier.notify({ name:"fruits.*", indexes });
      });
    }
  }
}

const viewModelProxy = new Proxy(new AppViewModel, new ProxyHandler);
viewModelProxy["fruits.0"]; // バナナ
viewModelProxy["fruits.1"]; // りんご
viewModelProxy["fruits.2"]; // オレンジ

欠点

  • フレームワーク内部でviewModelをすべてviewModelProxyへ変更する手間
  • プロパティ内のthisは、viewModelProxyにする必要があるので、アロー関数は使わない
  • プロパティが一致するかの判定の処理が増える

実際の実装では、"fruits.*"を機械的に正規表現"^fruits\.([0-9a-zA-Z_]+)$"へ変換する。また、"fruits.0"のマッチ結果をキャッシュに保存しておき、一致するかの判定処理のコストを下げる。

Javascript
class AppViewModel {
  "@@prop";
  // アロー関数は使わない
  "#eventClickAdd" = () => {
    this.prop = null; // thisがAppViewModelなのでpropの更新通知が飛ばない
  };
  "#eventClickAdd"() {
    this.prop = null; 
  };
}

感想

viewModelクラスでアロー関数を使えなくなるのはちょっと痛いけど、バグの多かった配列プロパティを展開する処理を省けるのは大きい。

Discussion

ログインするとコメントできます