Vue.js の watch と nextTick を使って JSON をアニメーション表示

最終更新日
2017.11.16

データの変更を監視するウォッチャと DOM に反映され後に処理をする nextTick を使います。

ウィンドウのリサイズ処理について本家のフォーラムを調べていて、こちらの記事を見ていると $nextTick というのが使われていました。
たまに見かける nextTick ってどういう時に使うんだろう?とマニュアルを見てみると以下のような説明が書いてあります。

callback の実行を遅延し、DOM の更新サイクル後に実行します。DOM の更新を待ち受けるためにいくつかのデータを更新した直後に使用してください。

vm.$nextTick( [callback] )

データが変わってそれが DOM に反映された後処理をさせたい時に使うようです。何か書いてみるのが一番はやいという事でこれを使って Ajax の処理を書いてみました。Ajax でランダムな長さのリストを受け取ってボックスに表示させます。あと各リストのアイテムをクリックするとそれだけ消えるようにしたいと思います。

Watch でデータの変化を監視する

watch(ウォッチャ)を使うとデータの変更を監視して登録した処理を行ってくれる。

まず、Ajax でデータを取得して message にセットします。message の内容が変わったらその都度高さを再取得する必要があるので、普通に書くとこんな感じかも。


// message をセットする処理...
this.getHeight() // 再取得
// message に追加する処理...
this.getHeight() // 再取得
// message を削除する処理...
this.getHeight() // 再取得

ウォッチャに処理を登録しておくと、この this.getHeight() を毎回書かなくてすみ「message が変わったなら頼まれてたこの処理もやっとくね」と勝手にやってくれます。


new Vue({
  watch: {
    // messages に変化があったらやりたい処理
    messages: function() {
      this.boxHeight = this.$refs.body.getBoundingClientRect().height
    }
  }
})

書き忘れる心配もない。消し忘れる心配もない。そうバグが生まれにくい!データのリアクティブと一緒で変化する場所が多いほど楽です。

nextTick で DOM 反映後に処理させる

高さが変わるアニメーションを付けたいと思いますが、実際に DOM が更新されてみないとそのコンテナの高さはわかりません。データを更新して画面に映し出されるまで一瞬の出来事に見えますが message を更新した直後に要素の高さを取得しても新しい DOM がレンダリングされていないので更新後の高さは取得できないのです。nextTick を使うと DOM が更新されたあとに確実に処理してくれます。

上で書いたウォッチャを毎回 DOM の反映後に処理させます。


new Vue({
  watch: {
    messages: function() {
      // DOMに反映された後に高さを取得
      this.$nextTick(function() {
        this.boxHeight = this.$refs.body.getBoundingClientRect().height
      })
    }
  }
})

出来上がったデモ

0~1秒でランダムに擬似的な遅延をしています。watchmessage を監視して内容が変わったらボックスの高さを変更するようにしています。

<div id="app">
  <p><button @click="getMessage" :disabled="isLoading">Get JSON</button> 各リストアイテムをクリックすると消えるよ!</p>
  <div id="box" :style="{height: boxHeight + 'px'}" :class="{loading: isLoading}">
    <div id="box-body" ref="body">
      <p v-for="(item, idx) in messages" @click="remove(idx)">{{ item }}</p>
    </div>
    <transition name="fade">
      <div class="overlay" v-show="isLoading"><span>LOADING NOW</span></div>
    </transition>
  </div>
  <p>BOXの高さ:{{ boxHeight }} px</p>
</div>

最初 Vue の transition:keyheight を指定してアニメーションさせようとしたんですけど、逆に非効率な感じだったので普通に CSS でやってます。

new Vue({
  el: '#app',
  data: function () {
    return {
      isLoading: false,
      messages: [],
      boxHeight: ''
    }
  },
  watch: {
    // messages に変化があったらする処理
    messages: function () {
      // DOMに反映された後に高さを取得
      this.$nextTick(function () {
        this.boxHeight = this.$refs.body.getBoundingClientRect().height;
      });
    }
  },
  methods: {
    // ajax で json データを取得して messages にセット
    getMessage: function () {
      var self = this;
      self.isLoading = true;
      axios.post('1.php').then(function (response) {
        self.messages = response.data.message;
      }).catch().then(function () {
        self.isLoading = false;
      });
    },
    remove: function (idx) {
      this.messages.splice(idx, 1)
    }
  },
  mounted: function () {
    this.messages = ['no message'];
  }
});

ん~なるほど watchnextTick もすごい便利ですね!

アセットのロードは待たない

遅延するのは DOM の更新を待つ間だけなので、重い画像のロードで後から高さが変わったりする場合などは流石に対応できないので、別途ロードを待つ処理が必要なのです。不特定なデータの高さを知るのは結構面倒くさいです。遅延ローディングなども考えると画像はなるべくサイズを指定しておいた方がいいでしょう。

どんな時に使うの?

具体的にこういう時みたいな例が書きづらいんですけど Vue で書いていると、データを使おうと思ったらなんか期待してる値じゃないよ?みたいな時がたまにあるのでだいたいそういう時に使います。あとはメソッド内で特定の処理を反映後にさせたい時に使ったりします。

私が最近使った例は、エディタ内の文字列を検索・置換させたあとにカーソルを次の検索結果位置に移動させたい時、置換が反映された後にカーソル位置を指定しないと文字数が変わってズレてしまうのでそれで使いました。ちょっとマニアックですね。

おわりに

すごく便利ですね!Vue.js の DOM 更新は非同期で行っているため直後に DOM の状態を取得できないようです。JavaScript の非同期というのはマルチスレッドとは違い一連の同期処理が終わった後に行うコールバックのようなもので、かならず同期処理の後に行われるようです。

ところで、最初に貼ったリンクで mounted 時のサイズ取得に nextTick を使っているのはなんででしょう。外しても同じ結果にみえるけど他で行われるかもしれない処理を考慮しているのでしょうか?もう少し詳しく調べてみたいと思います。

次はなにを勉強するか決まってませんが、SPA か状態管理についてそろそろやっていきたいなーと思います(•ө•)ノ