Vue.jsでリアクティブな独自データスコープを定義する

この記事は、Vue.js Advent Calender 2015 6日目の記事です。

# はじめに
Vue.js で、作成するコンポーネントにおいて `$data` とは別のスコープ(データ領域)を定義して、キレイにデータ分けて管理したいと思ったことありませんでしょうか？

例えば、サーバ側からフェッチしたデータ (モデル: Model) とコンポーネント内部で UI 制御のためのフラグ的なデータとか、フォームのバリデーションの結果を保持したデータとか。

この記事では、コンポーネントとは別にオレオレ的なデータスコープを定義して管理したいそんな方のために、リアクティブな独自スコープを定義する方法について紹介します。

# 注意
今回紹介する独自データスコープについてですが、実際にこの方法、Vue.js を使ったアプリケーション構築する際に、本当に必要かどうか**よく検討**してください。理由は以下のとおりです。

- [Vue.js 公式ドキュメント](http://vuejs.org)にはないアンドキュメント的なやり方である
- 内部 API を利用するため、Vue.js のバージョンアップによっては動かなくなる
- データスコープが増えるのでデータ管理が複雑になる
- アプリケーション設計の考慮事項が増える
- flux/redux などのデータフローアーキテクチャとの相性が悪い可能性がある

今回紹介する方法については、Vue.js を使って普通にアプリケーションを構築したい方というよりも、どちらかというと自分のような**プラグイン作成者向け**に役立つ方法です。なので、普通の方は、まあざっと参考程度に、以降読んで頂ければと思います。

# やり方
独自データスコープを定義する方法ですが、そんなに難しくありません。簡単です。以下のようにやります。

1. 独自に管理するデータを入れるオブジェクトを用意する
2. `1.`で用意したオブジェクトをリアクティブなプロパティとしてホストする Vue インスタンスを用意する
3. 内部 API `defineReactive` で`1.`と`2.`のオブジェクト、そしてプロパティキーをパラメータに指定して実行する
4. 後は、`Vue.set`、`Vue.delete` で`1.`のオブジェクトにゴニュゴニョする

上記をコードにするとこんな感じです。

```js
// 1. 独自に管理するデータを入れるオブジェクトを用意する
var myScope = {}

// 2. `1.`で用意したオブジェクトをリアクティブなプロパティとしてホストする Vue インスタンスを用意する
var vm = new Vue({ ... }) 

// 3. 内部 API `defineReactive` で`1.`と`2.`のオブジェクト、そしてプロパティキーをパラメータに指定して実行する
Vue.util.defineReactive(vm, '$my', myScope)

// 4. 後は、`Vue.set`、`Vue.delete` で`1.`のオブジェクトにゴニュゴニョする
Vue.set(myScope, 'foo', 1) // プロパティ'foo'を設定
Vue.set(myScope, 'bar', { buz: 'hello' }) // プロパティ'bar'を設定
...
Vue.delete(myScope, 'foo') // プロパティ'foo'の削除
```

そんなに難しいくないですよね？

ここでキモとなるのは、`defineReactive` です。このユーティリティ的な内部 API の呼び出しがないと、独自データスコープがリアクティブになりません。`defineReactive` は `$data` スコープの方でも使われており、Vue.js においてリアクティブシステムのコア的なユーティリティ関数となっています。`defineReactive` でホスト元になる Vue インスタンスにリアクティブを定義しておかないと、いくら独自データスコープで管理されているデータをいじっても、リアクティブにならず独自データスコープの内容がレンダリングされません。

また、`Vue.set` / `Vue.delete` の呼び出しも重要です。[Reactivity in Depth](http://vuejs.org/guide/reactivity.html) (日本語訳[リアクティブの探求](http://jp.vuejs.org/guide/reactivity.html))にも記載されているように、これらグローバルメソッドによって独自データスコープにプロパティを追加したり削除しないと、プロパティがリアクティブになりません。(ちなみに、`Vue.set` / `Vue.delete` は、`Vue.util.set` / `Vue.util.del` のエイリアスになっており、こちらの呼び出しでも同じです。)

最後に、やり方のまとめとして、リアクティブな独自データスコープをインスタンス毎に定義して、そのデータスコープにリアクティブなプロパティを追加/削除できるよう Vue インスタンスに、`$addProp` と `$delProp` というメソッドを生やす example 的なプラグインの例のコードを載せておきます。

```js
/**
 * 独自スコープを定義するプラグイン
 */
function plugin (Vue) {
  /**
   * _init をオーバーライドして、独自スコープを Vue インスタンス毎に初期化
   */
  var init = Vue.prototype._init
  Vue.prototype._init = function (options) {
    this._my = {} // 独自スコープのデータを初期化
    Vue.util.defineReactive(this, '$my', this._my) // 独自スコープ '$my' を定義

    init.call(this, options)
  }

  /**
   * 独自スコープに kye と value でプロパティを追加
   */
  Vue.prototype.$addProp = function (key, value) {
    Vue.set(this._my, key, value)
  }

  /**
   * 指定された key のプロパティを独自スコープから削除
   */
  Vue.prototype.$delProp = function (key) {
    Vue.delete(this._my, key)
  }
}
```

# デモ
上記のプラグインの example をデモとして動作させたものを作りました。こちらの gif で動作を確認できます。

![demo.gif](https://qiita-image-store.s3.amazonaws.com/0/7988/9a22d287-1870-0831-0e50-490cf04ea655.gif)

`$data` スコープの方が、Vue インスタンスのデータスコープ、`$my` スコープというのが、オレオレ的な独自データスコープとしてデータ内容を確認できるようになっています。`$my` スコープにプロパティを追加しても `$data` スコープに追加されず、`$my` スコープにのみ追加されているのを確認できるでしょう。また、削除も同様に、`$my` スコープからのみ削除されていることを確認できるでしょう。

# デモのコード
興味がある方のために、デモのコードはこちらにおいてあります。

- [vue-define-reactive-demo](https://github.com/kazupon/vue-define-reactive-demo)

# まとめ
Vue インスタンスの `$data` スコープとは別に独自データスコープを定義する方法を紹介しました。独自データスコープを定義することによって、`$data` スコープの方は、アプリケーションとして本質的なデータを管理できるようになります。

しかし、アプリケーションの実装として組み込む場合は、いろいろとデメリットがあるので、Vue.js をバージョンアップした際に、ある日アプリケーションが動かなくなるという、覚悟が必要です。

# おわりに
実はこのやり方は、[vue-validator](https://github.com/vuejs/vue-validator) という Vue.js 公式プラグインの開発・メンテナを担当している自分が、v2.0 の実装にあたって設計する際に生み出しました。フォームのバリデーションデータは、アプリケーションのデータとは明らかにドメインが異なっており、ユーザー側の `$data` スコープを汚したくなかったんで。

自分と同じような問題を抱えている場合は、ぜひ試してみてはいかがでしょうか？