ECMAScript2015のProxyを使ってobjectの値の変更を検出する

ECMAScript2015でobjectの値の変更を検出して関数を実行する場合、以前であればObject.prototype.watchの機能があったのですが現在では非推奨および廃止とのことなのでECMAScript2015から導入されたProxyの機能を使って値の変更を検出したいと思います。

MDNのサンプルでは以下のようにProxyに対して監視対象のobjectの初期値とイベントハンドラを渡していて、帰ってきたobjectの値を参照しようとした時にイベントハンドラのget関数が実行されていることが確認できます。

var handler = {
    get: function(target, name){
        return name in target?
            target[name] :
            37;
    }
};

var p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b); // 1, undefined
console.log('c' in p, p.c); // false, 37

上の例ではイベントハンドラにget関数を指定していましたが、set関数を指定すれば値の変更時に関数が呼び出されます。

set関数を指定して値の変更を検出するサンプルは以下のようになります。

class Counter{
  constructor(count){
    this.count = count;
  }

  countUp(){
    this.count += 1;
  }
  countDown(){
    this.count -= 1;
  }
}

const count1 = new Counter(0);

const watchCount = new Proxy(count1, {
  set(target, propertyKey, newValue, receiver) {
    const oldValue = target[propertyKey];
    const ret = Reflect.set(target, propertyKey, newValue, receiver);
    console.info(newValue);
    console.info(oldValue);
    return ret;
  }
});

watchCount.countUp();

このようにset関数を適用したイベントハンドラをProxyに渡すことでターゲットとなるobjectの変更じに処理を行うことができます。

ただ値の変更時に行う処理に時間がかかる場合、上記の書き方だと処理をブロックして進まなくなるのでEventEmitterを使った対応が考えられます。EventEmitterを使う場合、set関数を実行する時にemitするようにします。

import { EventEmitter } from 'events';

class Counter{
  constructor(count){
    this.count = count;
  }

  countUp(){
    this.count += 1;
  }
  countDown(){
    this.count -= 1;
  }
}

const count1 = new Counter(0);

const dispatcher = new EventEmitter();
dispatcher.addListener('change:count', (newVal, oldVal) => {
  window.setTimeout(() => {
    console.info(newVal);
    console.info(oldVal);
  }, 1000);
});


const stateWithDispatcher = (state) => (dispatcher) => {
  return new Proxy(state, {
    set(target, propertyKey, newValue, receiver) {
      const oldValue = target[propertyKey];
      const ret = Reflect.set(target, propertyKey, newValue, receiver);
      dispatcher.emit(`change:${propertyKey}`, newValue, oldValue);
      return ret;
    }
  });
};

const watchCount = stateWithDispatcher(count1)(dispatcher);
watchCount.countUp();