Vue.js が data に渡した値を激しく書き換える件について

最近、JavaScript の MV* フレームワークの中で Vue.js が少しずつ注目を浴びてきているようであります。

そんなわけで、自分も Vue.js (v0.10.5) を触ってみたのですが、data で渡した値を激しく書き換えるところに面食らったので記事にしておきます。

自作クラスのオブジェクトを Vue.js に渡すと壊される

何らかのビジネスロジックを持ったモデルを作って、それを Vue.js のデータバインディングで HTML に反映しようすると破綻します。

簡単な例として、よくある Animal クラスを作ったとしましょう。

function Animal(name) {
  this.name = name;
};
Animal.prototype.say = function() {
  console.log(this.name);
};

var dog = new Animal('dog');
dog.bark(); // I'm a dog

まぁ、当然動きます。

では、dog をデータバインディングで HTML に表示してみます。

<body>
<p>{{animal.name}}</p>
<script src="vue.js"></script>
<script>
function Animal(name) {
  this.name = name;
};
Animal.prototype.bark = function() {
  console.log("I'm a " + this.name);
};

var dog = new Animal('dog');
dog.bark(); // I'm a dog

new Vue({
  el: "body",
  data: { animal: dog }
});
</script>
</body>

期待通り、{{animal.name}} の部分が dog になります。

JavaScript コンソールにて、dog.name = "dog!!!" とすると、HTML は dog!!! に書き換わります。ちゃんと動いてるようにみえますね。

しかし・・・JavaScript コンソールで dog.bark() と入力すると・・・。

さっきまで動いていたコードが動かなくなりました・・・。恐怖!

激しく書き換えられた犬と書き換えられる前の猫

Vue.js では、data に渡した値を書き換えます。激しく書き換えます。

ためしに、dog を表示してみます。

ははは。プロトタイプ (__proto__) が別のオブジェクトになってしまっていますよ。bark() がなくてエラーになるのも無理はありません。

ちなみに、普通にインスタンス化した猫はこうなってますよ。当たり前だけども、bark() あります。

Vue.js さんは、new Vue()data に渡した値を書き換えてしまうのです。恐ろしい子!

なぜにあなたは書き換える?

Vue.js は data に変更が加わったことを検知するために、data の値を書き換えているようです。

たとえば、dog.name というキーは ECMAScript 5 のプロパティーに置き換えられます。get nameset name が定義されてますね。

このようにすることで、dog.name が書き換えられた瞬間に set name が呼ばれるので、Vue.js はデータの書き換えを検知するわけでございます。

じゃ、なんで prototype まで置き換えるのか。

Displaying a List - vue.js によると、ECMAScript 5 ではキーの追加・削除は検知できないので、$add$delete を使ってくれ、ということのようです (ざっくりと日本語訳してみた)。

In ECMAScript 5 there is no way to detect when a new property is added to an Object, or when a property is deleted from an Object. To counter for that, observed objects will be augmented with two methods: $add(key, value) and $delete(key). These methods can be used to add / delete properties from observed objects while triggering the desired View updates.

ECMAScript 5 では、Object に対するプロパティーの追加や削除を検出する方法がないんだぜ。だから、監視対象の Object には $add(key, value) と $delete(key) の 2 つのメソッドを追加するんだよ。このメソッドを使ってプロパティーを追加・削除すると、View への反映をトリガーできるんだぜ。

$add$delete を追加するのには、そういう理由があったわけですね。

さて、自作のクラスを渡せないのは明らかにバグっぽいので修正したいところではあります。

(追記) よくよく公式ドキュメントの Instantiation Options - vue.js を読んでみると

The object must be JSON-compliant (no circular references)

data に渡すオブジェクトは JSON の仕様に従っていて、循環参照してないものにしてね

とあるので、自作クラスのオブジェクトを渡せないのは仕方ないようです。

Array も激しく書き換える

Vue.js が激しく書き換えるのは Object だけではありません。配列も猛烈に書き換えます。

そのことは、ドキュメントの Displaying a List - vue.js からも伝わってきます (先ほどと同じくざっくりと日本語訳してる)。

Under the hood, Vue.js intercepts an observed Array's mutating methods (push(), pop(), shift(), unshift(), splice(), sort() and reverse()) so they will also trigger View updates.

You should avoid directly setting elements of a data-bound Array with indices, because those changes will not be picked up by Vue.js. Instead, use the agumented $set() method.

内部的に、Vue.js は Array の状態を変更するメソッド (push(), pop(), shift(), unshift(), splice(), sort() and reverse()) の呼び出しを監視して、View が更新されるように処理をしてるんだぜ。

インデックスを指定しての値を変更すると Vue.js が検知できないのでやめてほしいよ。その代り、$set() メソッドってのを用意したから、こっちを使ってほしいんだぜ。

まぁ、こんな感じで、扱うには少し工夫が必要であります。

data に渡した Object が console.log() で見にくい問題の対処方法

data に渡した値を console.log() すると getter, setter の山になるわけで、値がどうなっているかを確認しにくいですね。

これをなんとかするには JSON.stringify を使う回避方法が Instantiation Options - vue.js にて示されております。

var vm = new Vue({
  el: "body",
  data: {
    animal: { name: "foo" }
  }
});

console.log(JSON.stringify(vm.$data));
// {"animal":{"name":"foo"}} 

やや面倒だし、巨大なデータを渡したときは見つけるのが大変そうであります。そんなときは、さらに JSON.parse() で Object に変換すればよい。

面倒だから、Object に復元する関数でも作っておくとよいでしょう。

function deepCopy(o) {
  return JSON.parse(JSON.stringify(o));
}

console.log(deepCopy(vm.$data));

Object.observe に期待

こんな面倒なことになっているのも、Vue.js が ECMAScript 5 の世界で頑張っているからです。

ECMAScript に提案されている Object.observe() が使える世界になれば、Object や Array の置き換えも不要になるし、Vue.js の設計もシンプルになることでしょう。

Vue.jsロードマップ には 0.11 で Object.observe() が使えるなら使うようにする、と書いてあります。

まとめ

  • Vue.js はオブジェクトの監視を行うために、Object のキーをプロパティーに置き換えたり、Array.push() を置き換えたりする
  • この努力は ECMAScript 5 の限界ゆえ
  • ECMAScript に Object.observe() がやってきたら、Vue.js の実装もシンプルになるし、もっと楽な API になる