2018.05.20

Vue.jsコンポーネント入門 (7) ライフサイクルフック


本記事は Vue.js コンポーネント入門の第7回(最終回)です。

ライフサイクルフックを紹介します。

前提

PC ブラウザ Node npm Vue
macOS 10.12 Firefox Quantum 9.3.0 5.5.1 2.5

ライフサイクルフックとは

ライフサイクルフックとは、Vue コンポーネントが初期化されてから破棄されるまでの各タイミングで任意の処理を実行できるしくみです。

jQuery 風に言うと $(document).ready() のようなものでしょうか。画面が初期化されたタイミングで Ajax でデータを取得してリストを構築したいとか、jQuery プラグインの初期化を実行したいとか、そういう開発ニーズがありますよね。

さて、ライフサイクルフックの実例はこのようなものです:

ライフサイクルの例
<script>
  export default {
    data () {
      return {
        users: [], // ユーザーのリスト
        // ...
      }
    },
    // created フックは Vue インスタンスが作成された直後に呼ばれる
    created () {
      // つまり Vue インスタンスが作成されたタイミングで API にアクセスし...
      axios.get('/api/users')
        .then(response => {
          // データを取得してユーザーリストに代入する
          // ちなみにフック内での this はコンポーネントを指す
          this.users = response.data
          // これにより UI が更新される
          // 例えばユーザーのテーブルや連絡先カードのリストなど
        })
        .error(err => {
          // ...
        })
    }
  }
</script>

Vue コンポーネントのライフサイクルは大きく 4 段階に分かれます。

(1) Vue インスタンスの生成

フック:beforeCreate created

まずは Vue インスタンスが生成されます。具体的には、new Vue() が実行されたということです。この時点ではあくまで新しい JavaScript オブジェクトが作成されただけです。画面にはまだコンポーネントの内容は反映されていません。

インスタンスの生成前に beforeCreate フックが呼ばれ、インスタンスの生成後に created フックが呼ばれます。

(2) DOM へのマウント

フック:beforeMount mounted

生成された Vue インスタンスは、続いてコンポーネントとして DOM にマウントされます。つまり、コンポーネントが HTML 要素の一員として画面に描画されるということです。

マウント前に beforeMount フックが呼ばれ、マウント後に mounted フックが呼ばれます。

(3) データの変更〜画面の更新

フック:beforeUpdate updated

コンポーネントが DOM にマウントされて画面に描画された後は、コンポーネントはデータの変更を待ち受けます。コンポーネントの data が内部的に変更された場合や、props で渡されているデータが親コンポーネント側で(つまり外部的に)変更された場合には、それらの値の変更に合わせた形で画面も自動的に更新し描きかえます。

画面が描きかわる前に beforeUpdate フックが呼ばれ、画面が描きかわった後に updated フックが呼ばれます。

(4) Vue インスタンスの破棄

フック:beforeDestroy destroyed

v-ifv-for の機能によりインスタンスが表示されなくなるタイミングで destroy 系のフックが呼ばれます。

インスタンスが破棄される前に beforeDestroy フックが呼ばれ、インスタンスが破棄された後に destroyed フックが呼ばれます。

そのほかのフック

大きくは上記の 4 段階に分かれますが、そのほかにもフックが用意されています。activated deactivated errorCaptured です。応用的な内容ですので今回は説明を割愛します。

全てのライフサイクルフックについては、ライフサイクルダイアグラム および API マニュアルをご覧ください。

created と mounted のユースケース

実際の開発シーンでは最もよく使うであろう createdmounted について、ユースケースの例や使い分けかたを説明していきます。

ポイントは created のタイミングでは DOM にアクセスできないが、mounted のタイミングでは DOM にアクセスできるという点です。

created

ユースケース例:Ajax 通信を行いデータを初期化する

データベースと繋がるシングルページアプリケーションを作成する際には頻出なパターンでしょう。コード例は前節「ライフサイクルフックとは」の冒頭に挙げた通りです。

初期化のタイミングという意味では mounted で Ajax 通信を行ってもほぼ同じように動きますが、なぜ created の方がいいのかというと、created の方がタイミング的に(ごく僅かではあれ)前だからです。少しでも早く非同期通信を始めておいた方が、DOM マウントが行われ画面描画の準備が完了してからデータを画面に反映するまでの待ち時間が減るでしょう。

mounted

ユースケース例: Vue ではないプラグインを初期化する

世の中には便利な JavaScript ライブラリがたくさん存在しますが、そのほとんどがピュアな JavaScript で書かれているかもしくは jQuery プラグインでしょう。これらのライブラリは Vue コンポーネントの中でも簡単に使うことができますが、初期化の際にはターゲットとなる DOM 要素が必要とされることがあります。

例)select2
$('.js-example').select2();
例)ScrollMagic
var ctrl = new ScrollMagic.Controller();
new ScrollMagic.Scene({ duration: 100, offset: 50 }).setPin("#my-element").addTo(ctrl);

そういった場合には mounted に初期化処理を記述します。created ではまだ DOM にアクセスできないので、上記でいうと例えば "#my-element" を取得できないのです。

マニュアルの「ラッパーコンポーネント の例」がまさにこれです。

mounted: function () {
  var vm = this
  $(this.$el)
    // init select2
    .select2({ data: this.options })
    .val(this.value)
    .trigger('change')
    // emit event on change.
    .on('change', function () {
      vm.$emit('input', this.value)
    })
}

ちなみに上記の例で利用されている this.$el は、自分の Vue コンポーネントのルート DOM 要素を返します。

<template>
  <select>
    <slot></slot>
  </select>
</template>

上記のテンプレートの場合は this.$el<select> 要素を返します。ただし mounted より前のタイミングではまだ DOM にマウントされていないため、$el も利用できないということです。

HTML 要素の高さや幅を取得する場合も同様で、DOM 要素にアクセスする必要がある初期化処理は mounted に記述しましょう。

サンプルアプリケーション

第5回で作成したマークダウンコメントパネルに少し手を加えます。SimpleMDE という便利なライブラリを見つけたので、入力欄にそちらを適用して改良します。

前回は簡素なテキストエリアでしたが、少しそれっぽい見た目になったのではないでしょうか。

追加パッケージのインストール

# 第5回で作成したchapter-5ディレクトリで作業します。
$ npm install --save simplemde

入力欄コンポーネント

変更するのは入力欄コンポーネント(MarkdownEditor.vue)のみです。

src/components/MarkdownEditor.vue
<template>
  <div class="form-group">
    <textarea id="MarkdownEditorTextarea"></textarea>
  </div>
</template>

<script>
import SimpleMDE from "simplemde"

export default {
  props: {
    value: { type: String, required: true }
  },
  data () {
    return {
      editor: null
    }
  },
  mounted () {
    this.editor = new SimpleMDE({
      element: document.getElementById("MarkdownEditorTextarea"),
      status: false,
      toolbar: [
        "bold", "italic", "heading", "|",
        "quote", "unordered-list", "ordered-list", "|",
        "link", "image", "|",
        "guide"
      ]
    })
    this.editor.codemirror.on("change", () => {
      this.$emit("input", this.editor.value())
    })
  },
  updated () {
    this.editor.value(this.value)
  }
}
</script>

SimpleMDE をコンポーネントに適合させるため、mounted フックを使いました。

まず mounted で、SimpleMDE エディターを適用させる textarea に対して初期化を行っています。

this.editor = new SimpleMDE({
  element: document.getElementById("MarkdownEditorTextarea"),
  // 以下略

また、エディター内のデータが変更するたびに『 input イベントで親コンポーネントに入力値を通知する』処理を登録しています。

this.editor.codemirror.on("change", () => {
  this.$emit("input", this.editor.value())
})

余談

今回は第5回のサンプルを改良しましたが、コンポーネントを分割しておいたおかげで変更箇所が特定しやすく、また他のコンポーネントに影響を与えていない点に注目してください。

入力欄の変更の影響は入力欄コンポーネントに局所化されています。うまく部品化できているということです。入力欄コンポーネントは v-model に対応する、つまり『 value を受け取り input イベント発行で入力値を通知する』というインターフェースを守っている限りはどのように見た目や機能を変えても他の部品に影響を与えることはありません。

今回のテーマであるライフサイクルフックからは離れた話題でしたが、部品化の概念を理解するきっかけになればと思います。


本記事では Vue.js コンポーネント入門の第7回(最終回)としてライフサイクルフックを紹介しました。

まだ紹介していない機能はたくさんあります。しかしながら基本的な機能は紹介していますので、次または現在進行中のプロジェクトに導入する足がかりになれば幸いです。

というか基本的な機能だけでも結構パワフルなので、紹介した機能を組み合わせて応用するだけでも色々なものが作れるのではないでしょうか。

あとはマニュアルを読み進めれば発展的な機能まで把握できるはずです。最近は技術書も出版され始めているようですが、Vue のマニュアルは内容も豊富ですし最新版がすぐ日本語に翻訳されているので主な情報ソースとして十分だと思います。

関連記事

Vue.js コンポーネント入門(全7回)

Vue.js 入門(全7回)