これはcloudpack あら便利カレンダー 2018 の記事です。

Vue.jsでDOMが更新されない問題はわりとよくあたります。
ちょうど昨日社内で相談されたので、せっかくなので記事にまとめてみました。

ビューが更新されないあるある

配列、オブジェクトの更新が検出できない

:confused: 配列のケース

1.インデックスでアイテムを直接設定するとき 例: vm.items[indexOfItem] = newValue
2.配列の長さを変更するとき 例: vm.items.length = newLength

:relaxed: 解決方法
1. $setを使う
2. Vueが監視出来る配列のメソッドを使う push(), pop(), shift(), unshift(), splice(), sort(), reverse()

参考リンク => https://jp.vuejs.org/v2/guide/reactivity.html#配列の変化を検出

:confused: オブジェクトのケース

1.プロパティを追加・削除したとき

:relaxed: 解決方法
1. $setを使う
2. Object.assignで新しいオブジェクトとして入れ直す

参考リンク => https://jp.vuejs.org/v2/guide/reactivity.html#変更検出の注意事項

new Vue({
  el: "#app",
  data: {
    user: {
      name: "Nobu",
      icon: "https://benri2018.rnd.cloudpack.jp/images/2p.png",
      description: "Vue大好き"
    },
    fruits: [
        'apple', 'banana', 'lemon', 'orange'
    ],
    colorSchemes: [
        'red', 'blue', 'green', 'black'
    ]
  },
  methods: {
    toggle: function(todo){
        todo.done = !todo.done
    },
    addColor(color) {
        // userにcolorを追加してもビューがかわらない
        this.user.color = color;

      // $setを利用する、もしくはオブジェクトを入れ直すことで反映される
      // this.$set(this.user, "color", color);
      // this.user = Object.assign({}, this.user, { color });
    },
    changeArrayItem(index) {
        // インデックスでアイテムを直接指定してもビューが変わらない
      this.fruits[index] = "変更済み";

      // $setを利用する、Vueが監視出来る配列のメソッドを使う
      // this.$set(this.fruits, index, "$setで変更済み");
      // this.fruits.splice(index, 1, "spliceで変更済み");
    }
  }
})

サンプルコード => https://jsfiddle.net/nobu222/9nf7mcLx/

DOMが再レンダリングされず、変更前の値が残っている

:confused: Vue.jsのレンダリングについて

Vue.jsはDOM要素を可能な限り効率的に描画しようとします。
公式に挙げられている典型的な例です。
以下のような場合v-ifによりテンプレートが切り替えられますが、
<input>要素は再描画されず、input要素に入力値がそのまま残ります。

<template v-if="loginType === 'username'">
  <label>Username</label>
  <input placeholder="Enter your username">
</template>
<template v-else>
  <label>Email</label>
  <input placeholder="Enter your email address">
</template>

このVueでのDOMレンダリングが行われるタイミングを理解していないと
他のライブラリ(たとえばjQueryプラグイン!)を併用しているとき、
ライブラリの初期化処理がうまくいかなかったり、DOMが本来持っている初期動作がなされず
意図しない動作になってしまいます。

:relaxed: 解決方法
1. v-ifでDOMを再レンダリングさせる (v-ifのレンダリングは遅延レンダリングなので、値がtrueになって初めてレンダリングされます)
2. key属性を使ってDOMを再利用させないようにする

以下の様にkeyを利用することで入力値が毎回更新されるようになります

<!-- ユニークな値をkeyに設定することでDOMのレンダリングを制御できます-->
<template v-if="loginType === 'username'">
  <label>Username</label>
  <input placeholder="Enter your username" key="input-email">
</template>
<template v-else>
  <label>Email</label>
  <input placeholder="Enter your email address" key="input-email">
</template>

DOMの更新が非同期キューであることが考慮されていない

:confused: Vueでバインドされた値を変更した直後にVue外からアクセスすると変更されていない

バインドされた値の更新による、DOMの更新は非同期でなされます。内部的にはPromiseやsetTimeoutを利用し非同期に更新がキューイングされています。
以下の例はあまりピンと来ないものですが、これはDOMの状態に依存する他のライブラリなどを併用しているときなど起こりがちです。

意図しない挙動をする例

new Vue({
  el: "#app",
  data: {
    message: "not updated."
  },
  computed: {
    playVideo() {
        return this.videos[this.playVideoIndex]
    }
  },
  methods: {
    update() {
      this.message = 'updated.'
      const msg = document.getElementById('msg');
      console.log(msg.textContent); // log is 'not updated.'
    }
  }
})

:relaxed: 解決方法
1. nextTickを使う

nextTickは実行時点から次のキューイングが実行されDOMが更新されたタイミングでコールバック関数が呼び出されます。
さきほどの例だとspan要素のテキストが書き換わったタイミングで呼び出され、意図した動作で実行が出来ます。

new Vue({
  el: "#app",
  data: {
    message: "not updated."
  },
  computed: {
    playVideo() {
        return this.videos[this.playVideoIndex]
    }
  },
  methods: {
    update() {
      this.message = 'updated.'
      const msg = document.getElementById('msg');
      this.$nextTick(() => {
          console.log(msg.textContent); // log is 'updated.'
      })
    }
  }
})

サンプルコード => https://jsfiddle.net/nobu222/q1wrh7om/6/

以上、Vue.jsのデータバインディングとDOMのレンダリングで意図しない挙動をするよくある例を上げました。
今回の記事にあたって久しぶりにVue.jsのドキュメントをつらつらと見ていたのですが、
結構新しい発見がありました。(keyとかちゃんと知らんかった。。)
またそのへんはどこかでピックアップしたいと思います。それでは。

ほんとうは番外編でvue-routerで切り替えたコンポーネントは再利用されてcreatedとか発火しないですよーってのも書こうと思ってましたが、力尽きました。
なので参考リンクだけ => 動的ルートマッチング: パラメーター変更の検知

元記事はこちら

Vue.jsでビューの変更がされないときに疑うこと+主な解決策方法