Vue.jsのライフサイクルを完全に理解する - inokawablog
vuejs lifecycle

Vue.jsのライフサイクルを完全に理解する

コンポーネントが作成、DOMに追加され、更新、または破棄されるまでの間で、ライフサイクルフックを実行することができます。

vue.jsのライフサイクルは比較的簡単だと思うのでしっかり理解していきましょう。

vue.jsのライフサイクルを理解する

ライフサイクル図

こちらは公式サイトから拝借しています。

上から一つ一つ見ていきましょう。

new Vue()

vueインスタンスを生成しています。全ての Vue アプリケーション は、Vue関数で新しい Vue インスタンスを作成することによって起動されます。

Init Events & Lifecycle

beforeCreate

データの監視とイベント/ウォッチャのセットアップより前の、インスタンスが初期化されるときに同期的に呼ばれます。

インスタンスが生成される前に、
Javascriptオブジェクトが作成されただけなので、 DOMにはアクセスできず、画面にはコンポーネントの内容は反映されていません。

サンプル

<script>
new Vue({
  data(){
    return{
      hoge:"sample"
    }
  },
  beforeCreate () {
    console.log(this.hoge)
  }
})
</script>

結果

// ->何も表示されない

この例のようにbeforeCreateではdataにアクセスすることはできません。

created

インスタンスが作成された後に同期的に呼ばれます。この段階では、インスタンスは、データ監視、算出プロパティ、メソッド、watch/event コールバックらのオプションのセットアップ処理が完了したことを意味します。しかしながら、マウンティングの段階は未開始で、$el プロパティはまだ利用できません。

  • データ監視 : watch
  • 算出プロパティ : conputed
  • メソッド : methods

まだ、DOM 要素である$elにはアクセスできません。

<script>
export default {
  data() {
    return {
      hoge: 'sample'
    }
  },
  created() {
      console.log(this.hoge)
      console.log(this.$el)
  }
}
</script>
sample
undefined

beforeMount

render 関数が初めて呼び出されようと、マウンティングが開始される直前に呼ばれます。このフックはサーバサイドレンダリングでは呼ばれません。

beforeMountインスタンスが作成された後、elementへのマウントされる前で実行されます。

<script>
export default {
  beforeMount() {
    console.log(`まだ$elにはアクセスできません!!!!`)
  }
}
</script>

mounted

新たに作成される vm.$el によって置き換えられる el に対して、インスタンスがマウントされたちょうど後に呼ばれます。ルートインスタンスがドキュメントの中の要素にマウントされる場合、vm.$el も mounted が呼び出されるときにドキュメントの中に入ります。

コンポーネントが HTML 要素のとして画面に描画されます。
createdでは取得できなかったelementに対してアクセスできます。

そしてmountedで大事なのはここ。

mounted は 全ての子コンポーネントもマウントされていることを保証しないことに注意してください。ビュー全体がレンダリングされるまで待つ場合は、 mounted の代わりに vm.$nextTick を使うことができます。

そうmountedではthis.$nextTickを使うことができます!

vueは、dataの値を更新し、それをDOMに反映する処理を行う場合、更新処理を一つにまとめて、非同期的に処理していきます。

そのため、タイミングによっては正しい要素を取得できない場合などがあります。それを解決するのがnextTickです。

nextTickは簡単に言うとDOMの更新を待ってから実行するための処理です。

nextTickはまとまった処理を全て完了した後に実行されます。そのため、更新が全て終わった状態のものにアクセスできます。結構よく使います。便利。

new Vue({
  // ...
  methods: {
    // ...
    example: function () {
      // データを変更
      this.message = 'changed'
      // DOM はまだ更新されない
      this.$nextTick(function () {
        // DOM が更新された
        this.doSomethingElse()
      })
    }
  }
})

※そして、mountedはSSRでは使えないので注意してください。

beforeUpdate

データの更新があった時に、rerenderされる前に実行されます。更新前の既存のDOMに対してアクセスすることができます。DOMが更新される前に行いたい処理を記述します。

<script>
export default {
  data() {
    return {
      counter: 0
    }
  },
  beforeUpdate() {
    // DOMが更新される前に、カウンター値を毎秒ログ。
    console.log(this.counter)
  },
  created() {
    setInterval(() => {
      this.counter++
    }, 1000)
  }
}
</script>

また、beforeUpdateはSSRの場合は使えないので注意が必要です。

状態を更新し、仮想DOMが再描画される前に呼ばれる
つまり、beforeUpdate内で状態を取得すると更新後の値になっている。

ちなみに仮想DOMの説明はこちらがわかりやすかったです。vue.jsを使用する上で仮想DOMの概念は非常に大事になるので、ざっくりでもいいので理解しましょう。

updated

状態を更新し、仮想DOMが再描画される後に呼ばれる

コンポーネントのデータが変更され、仮想DOMが再描画される後に呼ばれます。プロパティの変更後にDOMにアクセスする必要がある場合updateで行なってください。

<template>
  <p ref="domElement">{{counter}}</p>
</template>
<script>
export default {
  data() {
    return {
      counter: 0
    }
  },
  updated() {
    // 毎秒ごとに発火
    console.log(+this.$refs.domElement.textContent === this.counter)
  },
  created() {
    setInterval(() => {
      this.counter++
    }, 1000)
  }
}
</script>

beforeDestroy

コンポーネント自体はまだ存在しています。イベントを破棄したりします。

Vue インスタンスが破棄される直前に呼ばれます。この段階ではインスタンスはまだ完全に機能しています。

vueの公式でいくつか使用している例があります。

これらはどちらもメモリリーク対策として使用していることがわかります。

メモリリークとは

確保したメモリ領域を解放する処理がプログラムに入っていないのが原因で、メモリを確保するけど解放しないのが続くことによって、メモリの空き領域が減っていくこと。

つまり、処理をするための作業スペースが減ってきてしまっているということ。

JavaScript自体ランタイム上で動的にメモリを確保するガベージコレクションを採用しているので、JavaScript の書き手がメモリの確保・開放を意識することは少ないですが、プログラムの書き方によっては、メモリリークに繋がります。

これは、Vue自体からは起こらず、むしろ他のライブラリをアプリケーションに組み込む際、発生する可能性があります。

実例

簡単な例では、このCodePenの例が用意されています。

例では、Choices.js で生成された追加 DOM 部分をクリーンアップできずに、メモリリークを引き起こします。

ターミナルを開いて何回かHideShowを繰り返すと、メモリ使用量が増え、再利用されないことが確認できます。

これを解決するには、Vue インスタンスのデータオブジェクトにプロパティを保持し、 Choices API の destroy() メソッドを使用してクリーンアップを実行します。

公式ではbeforeDestroyのタイミングで解決しています。

beforeDestroy: function () {
    this.choicesSelect.destroy()
}

こちらがメモリリーク改善後の例です。メモリ使用量が増えていかないのがわかります。

もう一つの例も同じように、Pikaday はサードパーティの日付選択のライブラリを使用した例も同じようにbeforeDestroyで破棄しています。

// 一旦DOMにマウントされたとき、
// datepicker をインプット要素に紐付ける
mounted: function () {
  // Pikaday はサードパーティの日付選択のライブラリです
  this.picker = new Pikaday({
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })
},
// コンポーネントが破棄させる直前に、
// datepicker も破棄されます
beforeDestroy: function () {
  this.picker.destroy()
}

こちらの例では、セットアップコードが、クリーンアップコードとは別に保たれているので、セットアップしたものをプログラムでクリーンアップすることをより困難にしています。つまり、作成はmountedですが、破棄はbeforeDestoryで行なっているのでよくないと。

これを解決するには、<input-datepicker> コンポーネントを作ることをおすすめします。

inputDatepickerComponent.vue

mounted: function () {
  var picker = new Pikaday({
    field: this.$refs.input,
    format: 'YYYY-MM-DD'
  })

  this.$once('hook:beforeDestroy', function () {
    picker.destroy()
  })
}

このようにすることで、いくつかのインプット要素で Pikaday を使用することができ、それぞれの新しいインスタンスは自動的にクリーンアップできます。とても素晴らしいですね!!!

こういった再利用性が高いからvue.jsは素晴らしいんですよね。

destroyed

Vue インスタンスが破棄された後に呼ばれます。このフックが呼ばれるとき、Vue インスタンスの全てのディレクティブはバウンドしておらず、全てのイベントリスナは削除されており、そして全ての子の Vue インスタンスは破棄されています。

destroyedExampleComponent.vue

<script>
export default {
  destroyed() {
    console.log(this) // 何も表示されない!!
  }
}
</script>

vue.jsのメインのライフサイクルはこれくらいになりますが、他にもあります。それが、activated,deactivated, errorCaptured です。長くなってしまうのでこれらは別記事で解説します。

Vue.jsのライフサイクルを完全に理解する 応用編

vue.jsのライフサイクルを完全に理解したところで、次はvue.jsのフレームワークである、nuxtのライフサイクルを見ていきましょう。

nuxtのライフサイクルを完全に理解する