Vue2 のリアクティブシステムをプレーンな Javascript で実装する

## 目的

`Object.defineProperty`の getter/setter を使って簡易的な Vue のリアクティブシステムを実装していくことがこの記事の目的です。実装は、Vue Mastery の「Build a Reactivity System」を参考にしています。

https://www.vuemastery.com/courses/advanced-components/build-a-reactivity-system

## 公式ドキュメントによると...

Vue2 のリアクティブシステムは、`Object.defineProperty`の getter/setter を使って実装されています。これについては Vue 公式ドキュメントの「リアクティブの探求」のセクションで触れられています。

> プレーンな JavaScript オブジェクトを data オプションとして Vue インスタンスに渡すとき、Vue はその全てのプロパティを渡り歩いて、それらを Object.defineProperty を使用して getter/setter に変換します。
https://jp.vuejs.org/v2/guide/reactivity.html

##  `Object.defineProperty` のド基礎

`Object.defineProperty`の公式ドキュメントです。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty


### ディスクリプターとは

Javascript のオブジェクトは、値（value）以外に詳細設定を持っています。詳細設定は、ディスクリプターと呼ばれており、例えば、書き換え可能なオブジェクトなのかどうか、などがあります。

```javascript
// ふつうのオブジェクト。値以外に詳細設定を持つ。
const obj = { name: 0 };
```

ディスクリプターは、`getOwnPropertyDescriptor`で確認することができ、ふつうにオブジェクトを定義した場合のデフォルト値は`true`になります。例えば、書き換え可能なオブジェクトかどうかを表すディスクリプターは`writable`であり、以下の例は、`true`なので書き換え可能である、ということになります。

```javascript
const obj = { name: 0 };
const descriptor = Object.getOwnPropertyDescriptor(obj, 'name')
console.log(descriptor)
// {value: 0, writable: true, enumerable: true, configurable: true}
```

`defineProperty`で定義した場合は、ふつうにオブジェクトを定義した場合の挙動と異なり、デフォルト値は`false`になります。

```javascript
const obj = {};
Object.defineProperty(obj, 'name', {
    value: 43
})
console.log(obj)
Object.getOwnPropertyDescriptor(obj, 'name')
// {value: 43, writable: false, enumerable: false, configurable: false}
```

###  getter/setter もディスクリプター
getter/setter は、オプショナルなディスクリプターで、デフォルト値は`undefined`なので、使いたい場合は追加で定義する必要があります。

```javascript
const obj = { _name: 'qiita' };
Object.defineProperty(obj, 'name', {
    get: function() {
        return this._name;
    },
    set: function (val) {
        this._name = val
    }
})
obj.name
// "qiita"

obj.name = 'twitter'
// "twitter"

obj.name
// "twitter"
```

`obj.name`では、`getter`経由で`qiita`という戻り値があり、`obj.name = 'twitter'`では、`setter`経由で`twitter`を書き換えています。

ここらへんから、けっこう Vue のリアクティブなコードの記法と一緒なので、これが Vue のリアクティブシステムのコアであると言われてるとわりとすんなり理解できる気がしています。

### 変更が追跡できないパターン

`Object.defineProperty`の getter/setter によってリアクティブシステムは実装されているため、 getter/setter のリアクティブの限界が Vue のリアクティブの限界でもあります（一部は Vue が補っている部分もあるため、完全なイコールではないです）。

例えば、配列への要素の追加の変更は追跡できないため、リアクティブになりません。

```javascript
var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // is NOT reactive
vm.items.length = 2 // is NOT reactive
```

## リアクティブシステムを Javascript で実装する

### 目的地

すごくシンプルな`price`と`quantity`の乗算によって、`totalPrice`を算出する Vue のコードを`Object.defineProperty`の getter/setter を使って実装していきます。

以下は、すごくシンプルな Vue のコードです。これと同じことを Javasciript で実装します。

```javascript
<div id="app">
    <div>Price: ${{ price }}</div>
    <div>Total: ${{ totalPrice }}</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
  var vm = new Vue({
    el: '#app',
    data: {
      price: 5.00,
      quantity: 2
    },
    computed: {
      totalPrice() {
        return this.price * this.quantity
      }
    }
  })
</script>
```

### 実装する ~その1~

まずは、オブジェクトの定義から書いていきます。

```javascript
let data = { price: 5, quantity: 2 }
```

次は、`price`あるいは`quantity`の値が変更されたときの再計算の関数を定義します.
`target`が呼ばれ、`data.total`の再計算が行われます。

```javascript
let target = null
target = function () {
    data.total = data.price * data.quantity
}
```

次に`data`に対して、getter/setter を設定します。

```javascript
let internalValue = data.price
Object.defineProperty(data, 'price', {
    get() {
        return internalValue
    },
    set(newVal) {
        internalValue = newVal
        target()
    }
})
```

現時点の動作を確認すると、`data.price = 10`の実行により、setter が呼ばれ、その中で`target()`が実行されることで、`data.total`が更新されています。

```javascript
data.total
// undefined
data.price = 10
// 10
data.total
// 20
```

ここまで実装すれば、リアクティブの実装の仕組みは理解できたはずです。

### 実装する ~その2~

このコーディングには以下の2点の問題があります。
- quantity のリアクティブの実装はまだしていない
- コードに柔軟性が足りない

`quantity`の getter/setter を定義したいですが、もう一つ`defineProperty`を書くのはナンセンスです。また、今は`price`の値が変更されれば`target()`を実行することを決め打ちで書いているので、`set()`に直接、`target()`を書いていますが、他の関数にしたいなどの要望があると追加しづらいです。

そのため、次は、依存関係を管理するクラスを定義します。
ようは、`data.price`の値が書き換えられたら、`target()`を実行する必要がある、という依存関係を管理します。  

`Dep`クラスを定義し、値が変更されたときに実行する関数（依存関係）を`depend()`によって`subscribers`リストに格納しています。これはあくまでも依存関係を取得しているのみであり、実際に値が変更されたときは、`notify()`によって、実行する関数を呼びます。  

```javascript
class Dep {
  constructor () {
    this.subscribers = [] 
  }
  depend() {
    if (target && !this.subscribers.includes(target)) {
      this.subscribers.push(target)
    } 
  }
  notify() {
    this.subscribers.forEach(sub => sub())
  }
}
```

`data`のキーが複数でも問題ないように`Object.keys`での実装に書き換えています。また、`get()`で`dep.depend()`を実行することで、依存関係を取得し、値が書き換えられる`set()`で`dep.notify()`を実行することで、実行する関数を読んでいます。

```javascript
Object.keys(data).forEach(key => {
    let internalValue = data[key]
    const dep = new Dep()

    Object.defineProperty(data, key, {
        get() {
            dep.depend()
            return internalValue
        },
        set(newVal) {
            internalValue = newVal
            dep.notify()

        }
    })
})
```

最後に`watcher()`を定義し、ここでやっと`target()`は実行されます。`watcher()`でラップしているのは、`target()`以外の関数でもいいようにです。

```javascript
function watcher(myFunc) {
    target = myFunc
    target()
    target = null
}

watcher(target)
```


### 最終成果物

実行結果。

```javascript
data.price = 50
// 50
data.total
// 100
data.quantity = 100
// 100
data.total
// 5000
```

コード。

```javascript
let data = { price: 5, quantity: 2 }

let target = null
target = function () {
    data.total = data.price * data.quantity
}

class Dep {
    constructor () {
    this.subscribers = [] 
    }
    depend() {
    if (target && !this.subscribers.includes(target)) {
        // Only if there is a target & it's not already subscribed
        this.subscribers.push(target)
    } 
    }
    notify() {
    this.subscribers.forEach(sub => sub())
    }
}

Object.keys(data).forEach(key => {
    let internalValue = data[key]
    const dep = new Dep()

    Object.defineProperty(data, key, {
        get() {
            dep.depend()
            // console.log('i was assessed', internalValue)
            return internalValue
        },
        set(newVal) {
            // console.log('i was changed', internalValue, newVal)
            internalValue = newVal
            dep.notify()

        }
    })
})

function watcher(myFunc) {
    target = myFunc
    target()
    target = null
}
watcher(target)
```


## まとめ

`defineProperty`の基礎から、簡単な Vue のリアクティブの実装をしました。
かなりシンプルですが、Vue のリアクティブシステムの実装ができたかと思います。

Vue3 からは、リアクティブの実装は`Proxy`オブジェクトを使用されているため、少し挙動は変わるようなので、次はそっちも見ていきたいです。