2018-05-13

改めてVue.jsのデータの監視方法と回避方法とコンポーネント設計

まとめ

  • Vue で監視していたのは data ではなく setter を通ったかどうかだった
    • そこは Backbone.js 時代から変わってなかった
  • Array や Object の一部を書き換える処理は setter を通らないので Vue は検知できない
  • 逆に丸ごと壊せば setter を通すことはできる
  • しかし恐らくその場合は書き換える DOM の範囲が広がり、VirtualDOM の速さのメリットがスポイルされる
  • コンポーネントを小さくしろというのはそういう意味でもあった

参考

Arrayの変更、Objectの変更はsetterを通らないので検知できない

まぁ、分かってしまえば当たり前の話なんですけど、こういう感じのコードがあると思いねぇ。

data() {
  return {
    model: {}
  }
},
methods: {
  action() {
    this.model['foo'] = 'bar'
  }
}

これ、data だからとか computed だからとか watch じゃないからとかじゃなくて、Vue では検知できないです。

これはごく簡単な理由で

this.model = {foo: 'bar'}

じゃないと setter を通らないから。つまり、

Vueは値は監視していない。setter が通知してるだけ。

そうと分かれば話は簡単。つまり Bachbone.Model の時代から本質は変わっていないわけです。あの頃は Model 側も event を通知する処理を自前で書いていましたが、その部分の記述が setter を通すことで省略できているだけ。

じゃあ検知させたければsetterを通せばよい

その通り。setter を通せばよいのです。

上のコードを本当に動くようにすると以下のようになります。

methods: {
  action() {
    let model    = JSON.parse(JSON.stringify(this.model))
    model['foo'] = 'bar'
    this.model   = model
  }
}

こうすれば setter を通るので複雑なデータ構造でも reactive system を動作させることができます。

でも関係するVirtualDOMは全書き換えになっちゃうし、影響も把握しにくい

当たり前ですね。そうなるとせっかく変更のある部分だけ書き換えることを容易に実現できる VirtualDOM の特長が犠牲になるわけです。例えば Array の変更を setter で通知してしまうと、v-for で回している部分は全書き換えになります。

さらに複雑で深い構造のデータを data として保持していて、もしそれがそのまま表示に関係している場合、その component と子の component など、多くの場所でこの data の書き換えの影響を受けます。言い換えると component が大きすぎるわけです。これは実行速度でもマイナスですが、変更もテストもやりにくくなります。1

素直にsetterで渡せる範囲がVue componentの適切な大きさの一つの指針なのかも

今回 setter の存在に改めて行き着いて、複雑な構造のデータの変更を無理矢理検知させることをやりましたが、本当はこれは Vue 的にはオススメできない方法のような気がします。

今回気づいた setter の動作が理想的なものなのか技術的な制約に基づく妥協なのかは分かりませんが、少なくともフレームワークにとって自然な動作を満たすようにしておくというのは、他の人とコードや知見を共有する際に有効です。逆にイレギュラーな使い方はこの共有を難しくします。

「component は小さく」とはよく言われますが、もしかしたらこの setter を意識せずに使える範囲こそが、component の適切な大きさなのかもしれないという、改めて考えるとごくごく当たり前のことに気づいたという話でした。

落穂拾い

以下、うまくハマらなかったものを殴り書きして終わります。

「component は小さく」とは言いますが、不必要に作りすぎても管理が大変です。小さく保つのは大事ですが、すべてを component に分ける必要はないと思います。

props バケツリレーは確かに面倒ですが、2, 3 階層程度なら割と慣れます。

props で渡すデータの数が増えてもあんまり気にしない方がいいかも。function の引数とは意味が異なると捉え、何も考えずに {{ }} で展開できる単純さを優先してもよいと思います。「責務が一つであればデータは一つでなくてもよい」という考え方。

  1. 例えば自動テストをするにしても初期化、stub out が面倒です。 

About

例によって個人のなんちゃらです