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

更新日
2017.07.28
作成日
2017.03.01

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

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

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

vm.$nextTick( [callback] )

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

Watch(ウォッチャ)でデータの変化を監視する

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

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

ウォッチャに処理を登録しておくと、この this.getHeight() を毎回書かなくてすみ「message が変わったなら頼まれてたこの処理もやっとくね」と勝手にやってくれます。データのリアクティブと一緒で変化する場所が多いほど楽です。

new Vue({
  watch: {
    // messages に変化があったらする処理
    messages: function() {
      this.boxHeight = document.getElementById('box-body').getBoundingClientRect().height;
    }
  }
});

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

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

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

new Vue({
  watch: {
    messages: function() {
      // DOMに反映された後に高さを取得
      this.$nextTick(function() {
        this.boxHeight = document.getElementById('box-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">
            <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 でやってます。

if (!window.Promise) {
  window.Promise = ES6Promise && ES6Promise.Promise;
}
new Vue({
  el: '#app',
  data: function() {
    return {
      isLoading: false,
      messages: [],
      boxHeight: ''
    }
  },
  watch: {
    // messages に変化があったらする処理
    messages: function() {
      // DOMに反映された後に高さを取得
      this.$nextTick(function() {
        this.boxHeight = document.getElementById('box-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 で書いてると使おうと思ったらなんか期待してる値じゃないよ?みたいな時がたまにあるのでだいたいそういう時に使います。あとはメソッド内で特定の処理を反映後にさせたい時に使ったりします。

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

気になる

でも、この nextTick ってどの DOM が更新されたかとか見てるわけじゃ無さそう?「~いくつかのデータを更新した直後に使用してください」という事はその時点で値が変わっているもの全部が DOM に反映された後に実行されるのでしょうか。具体的なパターンを思いつくわけじゃないけど丁度別の DOM が更新されたら…みたいな事はないのですかね。

ところで、最初に貼ったリンクで mounted 時のサイズ取得に nextTick を使っているのはなんででしょう。外しても同じ結果にみえるけど、ロード中によるサイドバーの有無とかのためでしょうか…?もう少し詳しく調べてみたいと思います。

まとめ

すごく便利で結構多用してます。

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