JSで値変更を検知したい-3（配列・Proxy編）

## はじめに
* 前回「[JSで値変更を検知したい-2（配列・defineProperty編）](https://qiita.com/ymgd-a/items/34f0ec606ab57459334a)」の続きです。
* 今回は 前回とは別の対応でオブジェクトのプロパティに配列が含まれていた場合の検知を検討してみます。
* 用意するもの、準備は前回の記事と同じです。



## 実装方針
* **Proxy** という仕組みが **ES6** から導入されています
    * [Proxy - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Proxy)
* 掻い摘むと オブジェクトの **様々な挙動** をカスタマイズできる仕組みです
    * 使い方は `defineProperty` と似ており、監視対象のオブジェクト（ターゲット）に対し、カスタマイズしたい動作（各種 ハンドラ）を定義します。
    * `defineProperty` との違いは **新しくインスタンス生成した `Proxy` オブジェクトに対し、操作をしなければならない** 点です。



## 実装１：まずは Proxy の挙動をチェック
* 以下のようなコードで `push()`, `pop()` した際の挙動を見てみましょう

### コード
```javascript
/**
 * 配列オブジェクトの動作を監視します
 * @param {Array} array 監視したい配列
 */
function watchArray(array) {
    // 戻り値として Proxy オブジェクトを返す
    return new Proxy(array, {
        // プロパティ削除時の動作をカスタマイズ
        deleteProperty: (target, property) => {
            console.log('\n** deleteProperty **');
            console.log(' - target: ', target);
            console.log(' - property: ', property);
            return Reflect.deleteProperty(target, property);
        },
        // プロパティ設定時の動作をカスタマイズ
        set: (target, property, val, receiver) => {
            console.log('\n** set **');
            console.log(' - target: ', target);
            console.log(' - val: ', val);
            console.log(' - property: ', property);
            console.log(' - receiver: ', receiver);
            return Reflect.set(target, property, val, receiver);
        }
    });
}

// 適当なオブジェクトを定義
const obj = {
    name: '太郎',
    clubs: ['Baseball', 'Tennis']
};

// 配列プロパティについて監視（NOTE: 戻り値の Proxyオブジェクト に対し操作する）
const watchedObj = watchArray(obj.clubs);

// 実際に変更してみる
console.log('\n■ pop します。');
watchedObj.pop();

console.log('\n■ push します。');
watchedObj.push('Soccer');

console.log('\n■ 値を変更 します。');
watchedObj[0] = 'Swimming';
```

### 実行結果
* 以下の通り出力されました。

```bash
■ pop します。

** deleteProperty **
 - target:  [ 'Baseball', 'Tennis' ]
 - property:  1

** set **
 - target:  [ 'Baseball', <1 empty item> ]
 - val:  1
 - property:  length
 - receiver:  [ 'Baseball', <1 empty item> ]

■ push します。

** set **
 - target:  [ 'Baseball' ]
 - val:  Soccer
 - property:  1
 - receiver:  [ 'Baseball' ]

** set **
 - target:  [ 'Baseball', 'Soccer' ]
 - val:  2
 - property:  length
 - receiver:  [ 'Baseball', 'Soccer' ]

■ 値を変更 します。

** set **
 - target:  [ 'Baseball', 'Soccer' ]
 - val:  Swimming
 - property:  0
 - receiver:  [ 'Baseball', 'Soccer' ]
```

### ポイント
* 配列オブジェクトを指定し `Proxy` インスタンスを生成し、各種操作は 戻り値の `Proxy` オブジェクトに対して行います。
* `Reflect` クラス は `Proxy` クラスと同一のメソッド（ただし静的）を持ちます。
    * 上記コードでは 既定の動作とその結果を返す 目的で使われます。
* 上記 実行結果 を見ることで、内部的にどのように配列オブジェクトに対する操作が行われているかトレースできます。
    * 配列に限らず、任意のオブジェクトに対し `Proxy` を使えば **JavaScript API が内部でどのような動きをしているか** もトレースできるかも・・・です
* 以下の通り、上記の動作をまとめてみました

#### pop() の動作
1. 配列の `1` というプロパティに対し `deleteProperty` が実行される（＝インデックスが1 の要素を削除）
1. 配列の `length` プロパティに対し `set` が実行される
    * 値として `1` をセットしています
    * 削除後の配列オブジェクトは `[ 'Baseball', <1 empty item> ]` となっています。
        * どうやら配列は内部的に `delete` 演算子で 削除されるため `empty item` といった結果となる模様？これは以下と同様の結果となります。

        ```javascript
        const a = [1, 2, 3];
        // delete 演算子を用いたプロパティ（配列特定要素）削除
        delete a[1];
        console.log(a); // [ 1, <1 empty item>, 3 ]
        ```

#### push() の動作
1. 配列に対し `set` が実行される（配列の `1` というプロパティ（＝インデックスが1 の要素）を削除）
1. 配列の `length` プロパティに対し `set` が実行される

#### 値変更の動作
1. 配列の `1` というプロパティに対し `set` が実行される



## 実装２：配列を監視する
* 実装１で `Proxy` を使用した場合の配列操作がなんとなく理解できました
* 次は実際に配列を監視してみましょう

### コード
```javascript
/**
 * 配列オブジェクトの動作を監視します
 * @param {Array} array 監視したい配列
 * @param {function(Array, Array)} onChange 変更時の動作
 */
function watchArray(array, onChange) {
    let deletedArray = null;
    return new Proxy(array, {
        // プロパティ削除時の動作をカスタマイズ
        deleteProperty: (target, property) => {
            // 削除操作呼び出し直後は empty item になるため、
            deletedArray = [...array];
            const result = Reflect.deleteProperty(target, property);
            return result;
        },
        // プロパティ設定時の動作をカスタマイズ
        set: (target, property, val, receiver) => {
            const oldArray = [...array];
            const result = Reflect.set(target, property, val, receiver);
            if (deletedArray) {
                // 削除操作を伴う場合の検知
                onChange(deletedArray, target);
                deletedArray = null;
            } else if (property !== 'length') {
                // その他：追加や変更の検知
                onChange(oldArray, target);
            }
            return result;
        },
    });
}

// 適当なオブジェクトを定義
const obj = {
    name: '太郎',
    clubs: ['Baseball', 'Tennis']
};

// 変更時に実行したい関数を定義
function onChange(v1, v2) {
    console.log(v1);
    console.log(' =>', v2);
    console.log('');
};

// 配列プロパティについて監視（NOTE: 戻り値の Proxyオブジェクト に対し操作する）
const watchedObj = watchArray(obj.clubs, onChange);

// 実際に変更してみる
console.log('\n■ pop します。');
watchedObj.pop();

console.log('\n■ push します。');
watchedObj.push('Soccer');

console.log('\n■ 値を変更 します。');
watchedObj[0] = 'Swimming';
```


### 実行結果
* 以下の通り 配列の各種操作に対して **変更前** と **変更後** の配列が取得できています

```bash
■ pop します。
[ 'Baseball', 'Tennis' ]
 => [ 'Baseball' ]


■ push します。
[ 'Baseball' ]
 => [ 'Baseball', 'Soccer' ]


■ 値を変更 します。
[ 'Baseball', 'Soccer' ]
 => [ 'Swimming', 'Soccer' ]
```


## 問題点

### `Proxy` のパフォーマンス問題
* かなり前に検証し、詳細は記憶の彼方… のためここに具体的なことが書けませんが、相当数のトラップを貼った際に動作がもっさりしていた記憶があります。。。
* 参考情報：
    * [Thoughts on ES6 Proxies Performance | www.thecodebarbarian.com](http://thecodebarbarian.com/thoughts-on-es6-proxies-performance)
    * 上記情報では 改良策 も掲示されていますが、詳細未検証です。。。いつかやりたい。。。

### shift, unshift 他のメソッド
* 一番の問題、他のメソッドでも本当にうまくいくのでしょうか。
* 上記実装２の最後の方を 以下の通り変更します。

```javascript
console.log('\n■ unshift します。');
watchedObj.unshift('Sumo');
```

* 実行結果は以下の通りです。

```bash
■ unshift します。
[ 'Swimming', 'Soccer' ]
 => [ 'Swimming', 'Soccer', 'Soccer' ]

[ 'Swimming', 'Soccer', 'Soccer' ]
 => [ 'Swimming', 'Swimming', 'Soccer' ]

[ 'Swimming', 'Swimming', 'Soccer' ]
 => [ 'Sumo', 'Swimming', 'Soccer' ]
```

* `unshift` というメソッドは 配列の先頭に値を挿入するメソッドです。
* 上記の実行結果から、内部的には、おそらく以下のような動作となっていることがわかります。
    1. 末尾の要素をコピーし、末尾の次に追加する
    2. 末尾から先頭に向かって順番に、一つ手前（先頭方向）の要素を現在の要素に上書きコピーする
    3. 先頭要素に、`unshift` の引数で指定された要素にする
* **１回の配列操作で何回も変更が検知される** ・・・という なかなか痛い問題点 はありますが、検知自体はできています
* この問題に対する解決方法は 悩み中ですが、物量が多くなったので [次回](https://qiita.com/ymgd-a/items/7e547fa78d2446cc8240) は一旦まとめに入ります。