Vue2.xで配列要素の追加/削除で変更検知する – $setや$deleteをリアクティブシステムを少し解説

こんにちは。てぃろです。

今回はVue.jsで配列操作しても画面描画が変わらない!というあるあるな状況に対応したときの備忘録です。

結論はこちら。

  • 要素を追加したいときは、Vue.setを使う
  • 要素を削除したいときは、Vue.deleteを使う

参考:
https://vuejs.org/v2/api/#Vue-set
https://vuejs.org/v2/api/#Vue-delete

ここからは簡単に変更検知の仕組みにも言及しながら解説していきます。

解説のもとにしているのは、Vue 2.xの内容です。

Vueが検出できない変更もある

Vueは暗黙的に変更検知するような仕組み(=リアクティブシステム)を備えています。

Vueのdataにプロパティを宣言すると実は勝手にgetter/setterを作っていて、プロパティへの値の変更や取得をそのgetter/setterを使うようにしています。このようにデータの変更がgetter/setterというVueの仕組みで行われている限り変更検知=再描画が可能ということです。

Reactivity Cycle
getter/setterを使ってデータ操作をするとWatcherに通知されて再描画がされる。
変更検知はWatcherに再描画の命令をさせることがポイント。
引用:https://jp.vuejs.org/v2/guide/reactivity.html

しかし、実はVueのリアクティブシステムを通さない変更もできてしまうのです。

そのような操作をすると、当たり前ですが、変更検知できないので再描画されず、画面に変更が反映されない、ということになるんですね。

ここからは公式ドキュメントを参考にしつつ、そのような変更検知できない場合を見ていきます。

ちなみに、リアクティブという言葉ですが、英語でリアクティブ(Reactive)とは反応性といった意味です。つまり、変更検知ができる、というような意味です。今後のために覚えておきましょう。

配列に要素を追加する場合は検知できない

以下のように、dataオブジェクト内に宣言した配列に対して新たな要素を追加する場合のことです。

var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // これは変更検知できません

配列itemsは最初に宣言しているのでVueのリアクティブシステム(=変更検知の仕組み)が変更検知の仕掛けを入れてくれていますが、配列の要素までは監視してくれているわけではないのです。

これは削除の場合も同様です。配列の要素をsplice()等で削除しても変更検知してくれません。

プロパティの追加または削除も検出できない

プロパティとは、dataオブジェクト内で宣言した変数のことです。この最初に宣言したプロパティ以外の変更検知はできないということです。

以下は公式ドキュメントからの引用です。ここでいうaが最初に宣言した変数で変更検知が可能なプロパティです。

var vm = new Vue({
  data: {
    a: 1
  }
})
// `vm.a` は今リアクティブです = 変更検知できる

vm.b = 2
// `vm.b` はリアクティブでは"ありません" = 変更検知できない

逆に、bのようにあとから追加した場合には変更検知はできません。

このように一般的なJavaScriptの書き方をしていては変更検知をしてほしくてもしてくれない場合があるのです。

そのような場合のために、変更検知を強制できるメソッドをVueは提供してくれています。

勝手に変更検知してくれないなら変更したと教えてやろう

先ほどまで見た変更検知してくれないケースはリアクティブシステムの限界です。しかし、変更検知をサポートするAPIを使うことによってその弱点を補うことができます。

今回紹介するAPIは、Vue.set( target, propertyName/index, value )Vue.delete( target, propertyName/index )です。

これらを使うことでリアクティブシステムの仕組みの中でプロパティの変更をしていくことができます。

使い方は以下のように簡単です。

var vm = new Vue({
  data: {
    a: 1,
    items: ['a', 'b', 'c']
  }
})

vm.set(data, b, 2) // 修正前:vm.b = 2
vm.delete(data, a)
vm.set(items, 1, 'x') // 修正前:vm.items[1] = 'x'
vm.delete(items, 'a')

上記とは違い、export defaultを使う場合もあるでしょう。私はそうなのですが、その場合には以下のようにthis.$setthis.$deleteを使います。

export default {
  data: {
    a: 1,
    items: ['a', 'b', 'c']
  }
}

this.$set(data, b, 2) // 修正前:vm.b = 2
this.$delete(data, a)
this.$set(items, 1, 'x') // 修正前:vm.items[1] = 'x'
this.$delete(items, 'a')

これらはVue.setVue.deleteのエイリアスなので同じメソッドを呼んでいることになります。

これで配列の要素の追加削除でも変更検知をして画面の再描画をしてくれるようになりました!

終わりに:既にVue3では改善されてます

最初にも書いた通り、こちらはVue 2.xの内容をもとに書いています。

すでにVue 3がリリースされており、こちらではこのあたりの機能が改善されています!配列要素の追加・削除をpush()やdeleteでも検知してくれるようになっています!すごい!

でも、いまだにdataオブジェクトにないプロパティの追加に関してはそのままでは検知してくれないみたいです。残念。

また、Vue3になるとリアクティブシステムについての解説も以前よりドキュメント量が大幅増量しています。

リアクティブシステムにもっと詳しくなりたい方は、是非Vue3の公式ドキュメントも読んでみて下さい。

Vue.jsに限らずAngularでも変更検知に関しては同じような作りになっています。以前Angularに関しても少しはまったことがあったので、その備忘録があります。こちらも参考にどうぞ。