2020-04-24

【Vue.js】オブジェクト(連想配列)をリアクティブにする

Vue.jsにて画面が再描画されない問題、つまりオブジェクトをリアクティブにする方法を分かりやすく解説。

Article Image

オブジェクトがリアクティブとは?

順を追って説明する。

まず次のようなData部がある。

  data: () => ({
    obj: {
      a: "A: OK",
    }
  }),

これをテンプレート部で「{{obj}}」として表示してやると

{ "a": "A: OK" }

と表示されるはずだ。

mounted

mounted()に次のコードを追加する。

  mounted(){
    this.obj.b = 0
  }

mountedはここでは理解する必要はない。

DOM要素がマウントされ画面が描画され、再描画される準備が整ったところと思っておけば良い(たぶん)

つまり obj"b" のキーを新規で追加したので

{ "a": "A: OK", "b": 0 }

となることを期待したコードだ。

しかし、実際は次の表示になる。

{ "a": "A: OK" }

これがリアクティブではない状態だ。初心者が躓く箇所である。

なぜ"b"は描画されないのだろうか。

デバッガで確認する

画面上はまだ"a"しか表示されていない状態でvueのデバッガを見てみる。

objはどうなっているだろうか(デバッガの使い方が分からなければconsole.log(obj)でもよい)

2020 04 24 14h14 37

間違いなく"b":0も追加されている。

しかし、画面には描画されていない。

リアクティブにするには

これは公式の「リアクティブの探求」ページに記載されている。

Vue はプロパティの追加または削除を検出できません。Vue はインスタンスの初期化中に getter/setter の変換を行うため、全てのプロパティは Vue が変換してリアクティブにできるように data オブジェクトに存在しなければなりません

読んでもよくわからないと思う。英語を日本語に直したものなのでイマイチピンとこない。

要するに、初めにdata部で定義されていた物を除いて後からオブジェクトにキーを追加しても再描画されないよということである。

では、どうすればよいのだろうか。

リアクティブなキーを追加するコードがある

深く考えずに次のコードを通してオブジェクトを更新してやれば良い。

  mounted(){
    //this.obj.b = 0
    this.$set(this.obj, "b", 0) 
  }

this.$set(オブジェクト, キー名, 値) このコードは this.obj.b = 0 と同じ意味合いでありながら、キー"b"をリアクティブにすることが出来るコードだ。

要するにキーを直接追加するのではなく一旦Vue本体を通して更新してやることでVueがリアクティブなオブジェクトとして認識できるようになったと考えておけば良い。

おまけ:配列をリアクティブに

「オブジェクトではなく配列をリアクティブにする」場合は配列に.splice()を付けて更新してやれば良い。

基本は以上で終了

基本はこれだけで終了だ。

ここからはリアクティブの「探求」

ちょっと別のコードで研究をしてみる。

リアクティブじゃないキー+リアクティブなキー

次のコードにしたらどうなるだろうか。

  mounted(){
    this.obj.b = 0
    this.obj.a = "A: Updated"
  }

"b"は非リアクティブ、"a"はリアクティブだ。

結果は

{ "a": "A: Updated", "b": 0 }

どうやらリアクティブな"a"が更新されたため、同じobj内の"b"の描画も更新された。

次の実験

これはどうか。

  mounted(){
    this.obj.b = 0
    this.obj.a = "A: Updated"
    this.obj.b = 1
  }

こんどは"b"0を入れて"a"を更新した後にさらに"b"を更新している。

これまでを読めば"b"はリアクティブではないので内部的には1だが描画は0なのではないかと思うが

{ "a": "A: Updated", "b": 1 }

しっかりと更新されている。

つまりmounted(){} 内がすべて実行された後に再描画される。

余談:これは一つの関数内で大量の再描画が必要そうなコードがあっても、纏めて1回更新にすることで無駄な描画負荷を減らすVueのアルゴリズムだ。

別のオブジェクトは?

なら次はどうだろう。

<template>
  <div id="app">
    {{obj}}
    {{uho}}
  </div>
</template>

uhoオブジェクトを追加した。

data部には先程と同じobjは記述してあるが、uhoオブジェクトは全く定義していない

その上で

  mounted(){
    this.uho = {
      a: "uho: ok"
    }
  }

uho.a"uho: ok"を入れた。

実行結果は

{ "a": "A: OK" }

uhoは表示されない。当然だ。ここまで学習していればuhoが描画されない理由は分かるだろう。

では、次のコードをに変更する。

  mounted(){
    this.obj.a = "A: Updated" //リアクティブなobj.aも一緒に更新
    this.uho = {
      a: "uho: ok"
    }
  }

さて、どうなる。

{ "a": "A: Updated" } { "a": "uho: ok" }

なるほど、uhoオブジェクトも更新された。リアクティブな何かが更新されれば、同時に別のリアクティブじゃない物も再描画されるらしい。

つまり

この関係性を正しく理解していないと、予期せずリアクティブな物と非リアクティブな物を混在した場合に時々再描画されないバグや、コードを変えた途端に描画が意図しないものになる可能性がある。

以上の理由から初めからキー構造をdata部に列挙しておくのが重要ということが分かる。値は空 '' 等でよい。

$nextTick()

この関係で直接DOMを操作するコードを使っている場合、同じファンクション内で再描画を明示的に待ってやるコード $nextTick() が必要となるパターンもある。

これは高度なテクニックであるため詳しくは説明しないがシンプルで分かりやすいサイトがあったので外部リンクを載せておく。

Vue.js で表示に切り替えたDOMにフォーカスできないときは $nextTick を使う

所感

実は度々この問題で頭を悩ませていた。今回これでクリアになったのでもう迷わないだろう。

Vueの少し難しい領域だが絶対に覚えておきたい項目だ。



この記事のタグ

この記事をシェア


謎の技術研究部 (謎技研)