LoginSignup
59
24

More than 3 years have passed since last update.

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

Last updated at Posted at 2017-08-19

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

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

Object.defineProperty メソッドを通してVue.jsのリアクティビティは実現されている、という内容です(ここでの「リアクティビティ」とは、モデルのデータ変更がリアルタイムでビューに伝播する仕組みのことをいいます)。

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

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

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

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

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

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

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

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

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

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

// リアクティビティに関係ある部分のみ抜粋
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]])
    }
  }

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

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

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

/**
 * 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()
    }
  })
}

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

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

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

自作してみる

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

<!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のdatav-modelを使った時の動作と同じですね!

JSFiddle にもソースコードを置いてあるので、遊んでみてください。

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

59
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
59
24