Object.definePropertyによるVue.jsのリアクティビティを紐解く

Vue.jsのドキュメントには以下のような記載があります。

> プレーンな JavaScript オブジェクトを data オプションとして Vue インスタンスに渡すとき、Vue.js はその全てのプロパティを渡り歩いて、それらを Object.defineProperty を使用して、getter/setter に変換します。これは ES5 だけのシム (shim) ができない機能で、Vue.js が IE8 以下をサポートしない理由です。

https://jp.vuejs.org/v2/guide/reactivity.html

[Object.defineProperty](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) メソッドを通してVue.jsのリアクティビティは実現されている、という内容です（ここでの「リアクティビティ」とは、モデルのデータ変更がリアルタイムでビューに伝播する仕組みのことをいいます）。

では、Vue.jsは `Object.defineProperty` をどのように使ってリアクティビティを実現しているのでしょうか？

## Vue.jsのソースコードを覗く

Vue.jsでは、以下のようにdataプロパティにオブジェクトを登録すると、そのオブジェクトのプロパティはリアクティブになります。

```js
const vm = new Vue({
  el: '#app',
  data: {
    message: 'Hello, World',
  }
});

// ここではvm.messageはリアクティブ
vm.message = 'Hello, Vue.js!';
```

これを実現するのは、Vue.jsの内部APIにある`initData`関数です。`initData`関数はVueのコンストラクタ内で呼ばれます。

```js
function initData (vm: Component) {
  let data = vm.$options.data
  // （中略）
  // observe data
  observe(data, true /* asRootData */)
}
```

https://github.com/vuejs/vue/blob/eb9168cfc1816b53ddb1eccd6310173a37803897/src/core/instance/state.js#L116

ここで`observe`関数が使われています。この関数は、以下のように`Observer`オブジェクトを生成します。

```js
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // valueの型に応じた処理等があるが、諸々省略すると以下のようになる
  return new Observer(value)
}
```

https://github.com/vuejs/vue/blob/eb9168cfc1816b53ddb1eccd6310173a37803897/src/core/observer/index.js#L106

さらに`Observer`オブジェクトのコンストラクタをたどると、`defineReactive`という関数を使っていることがわかります。核心に近づいてきました！

```js
// リアクティビティに関係ある部分のみ抜粋
export class Observer {
  constructor (value: any) {
    this.walk(value)
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
```

https://github.com/vuejs/vue/blob/eb9168cfc1816b53ddb1eccd6310173a37803897/src/core/observer/index.js#L34

ここでは、`Observer.walk`メソッドの中で、渡されたオブジェクトのキーを走査している点に注目です。先ほどのサンプルコードでは `{ data: { message: 'Hello, World' } }` というdataプロパティを定義しましたが、この場合、`defineReactive`関数は以下のように呼び出されます。

```js
const data = { message: 'Hello, World' };
defineRaactive(data, 'message', 'Hello, World');
```

下記が`defineReactive`関数の実装です。複雑ですが、結局やっていることは、`Object.defineProperty`メソッドを使って、第1引数のオブジェクトに、第2引数の名前のプロパティを設定しているだけです。

```js
/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
```

https://github.com/vuejs/vue/blob/eb9168cfc1816b53ddb1eccd6310173a37803897/src/core/observer/index.js#L131

リアクティビティにおいて特に重要なのは、値がセットされた時の動作、つまりsetterです。ここから先は、さらに値をDOM（に限らず、値の変更を監視しているオブジェクト）に通知する仕組みがありますが、省略します。

まとめると、以下のように呼び出しが行われて、最終的にリアクティブプロパティが設定されます。

1. `new Vue()`
2. `initData()`
3. `observe()`
4. `new Observe()`
5. `defineReactive()`

## 自作してみる

概略はわかりましたが、具体的にどういう実装になっているのかが若干わかりづらいと思います。そこで、同等の仕組みを自作してみましょう。

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>defineProperty</title>
</head>
<body>
    <h1 id="message"></h1>
    <input type="text" id="input">
    <script>
        // 下準備
        let message = 'Hello, World!';
        const data = {};
        const h1 = document.getElementById('message');
        const input = document.getElementById('input');
        h1.textContent = input.value = message;

        // リアクティブプロパティの定義
        Object.defineProperty(data, 'message', {
            get() {
                return message;
            },
            set(newVal) {
                message = newVal;
                h1.textContent = message;  
            }
        });

        // 入力値をdataオブジェクトに伝播
        input.addEventListener('input', (ev) => {
            data.message = ev.target.value;
        });
    </script>
</body>
</html>
```

このソースコードを動かすと、以下のように、入力値が即座に反映されるようになります。Vue.jsの`data`と`v-model`を使った時の動作と同じですね！

<img src="https://media.giphy.com/media/3o7OsLTWP9JxYrUmyY/giphy.gif">


[JSFiddle](https://jsfiddle.net/vas5fsgv/1/) にもソースコードを置いてあるので、遊んでみてください。

ちなみに、setで `message = newVal` ではなく `date.message = newVal` にしたらどうなるか…。興味のある人は試してみてください（ブラウザがクラッシュする恐れがあるので注意！）。