こんにちは。 株式会社エビリーの millvi 開発チームでエンジニアをしております。 井上と申します。
millvi ではフロントエンドの開発に Vue.js を採用しようと活動しております。
その活動の一環として、こちらのブログでも Vue.js での開発で学んだことをフィードバックしたいと思います。 よろしければお付き合いくださいませ。
※ 本記事は こちらの記事 の続きとなります。よろしければあわせて御覧ください。
※ 記事中のリポジトリは私的利用しているリポジトリになります。
はじめに
Vue.js におけるライフサイクルについて こちら でみた。 ただ 公式のこちら には上記の記事で扱った以外のフックとして以下の 3 つが取り上げられている。
- activated
- deactivated
- errorCaptured
本記事ではこれら 3 つのフックについて動きを見ていく。
お詫び
記事中、コンポーネントの名称について スクリーンショット と 文中の単語について表記ゆれがあります( Life Cycle
, LifeCycle
, Lifecycle
等 )が、同じコンポーネントを指しております。
お見苦しくて申し訳ありません。
前提
- Vue CLI でプロジェクトを作成していること
- 単一ファイルコンポーネントであること( 1 )
- 動作確認は
$ npm run serve
で起動した環境で行なっている activated
とdeactivated
の説明のために動的コンポーネントを扱っているが、動的コンポーネントそのものについては触れない
環境
Version | 備考 | |
---|---|---|
Vue | 2.6.11 | |
Vue CLI | 4.1.1 |
今回扱うライフサイクルフック
フック | タイミング | 備考 | |
---|---|---|---|
1 | activated | コンポーネントが活性化するとき | 同上 |
2 | deactivated | コンポーネントが非活性になるとき | 同上 |
3 | errorCaptured | エラーが発生したとき | 子孫コンポーネントからのエラーをキャッチする エラー伝播ルール を把握しておくこと |
activate と deactivate
前掲の公式ページによると、 activate
と deactivate
には次の 2 つが参照として挙がっている。
まず 組み込みコンポーネント の方を覗いてみると、次の説明がある。
[公式ページから引用]
動的コンポーネント周りでラップされるとき、
<keep-alive>
はそれらを破棄しないで非アクティブなコンポーネントのインスタンスをキャッシュします。<trasition>
に似ていて、<keep-alive>
はそれ自身 DOM 要素で描画されない抽象型コンポーネントです。activated
とdeactivated
ライフサイクルフックはそれに応じて呼び出されます。コンポーネントが
<keep-alive>
内部でトグルされるとき、activated
とdeactivated
ライフサイクルフックはそれに応じて呼び出されます。2.2.0 以降では、
<keep-alive>
ツリーの中の全てのネストされたコンポーネントに対して、activated
およびdeactivated
を発行します。主に、コンポーネント状態を保存したり、再描画を避けるために使用されます。
ということで、
activate
とdeactivate
はライフサイクルの仲間であり<keep-alive>
でコンポーネントを囲むことで、- 囲まれたコンポーネントは コンポーネントの切り替え が行われると
activate
とdeactivate
のフックが走る - このとき切り替えられたコンポーネントの Vue インスタンスは 破棄されずに残る
- 囲まれたコンポーネントは コンポーネントの切り替え が行われると
ことが分かる。 動的コンポーネント については、ここで取り上げると長くなるので割愛させていただく。前掲の公式ページ( 動的コンポーネント - keep-alive )を参照されたい。
では実際の動きをコードで見てみる。
確認用コードについて
- 動的コンポーネントの動きとあわせて見るため、親コンポーネントと 2 つの子コンポーネント、計 3 つのコンポーネントを用意した
- なお子コンポーネントについては、ログ出力の内容やコンポーネント名を除き、同じ構造である
- これは両者とも同じ構造の方が動作確認時に比較しやすいとの意図のもと、そうしている( 冗長ではあるがご容赦いただきたい )
親コンポーネント( LifeCycle2 )
<template> <div :class="$style.lifecycle2"> <div :class="$style.wrapchild"> <input id="Lifecycle21" v-model="selectedComponent" type="radio" value="Lifecycle21"> <label for="Lifecycle21">Lifecycle21</label> </div> <div :class="$style.wrapchild"> <input id="Lifecycle22" v-model="selectedComponent" type="radio" value="Lifecycle22"> <label for="Lifecycle22">Lifecycle22</label> </div> <h1>selected component is ... {{ selectedComponent }}.</h1> <keep-alive> <component :is="selectedComponent" :class="$style.lifecycle2_child_area"/> </keep-alive> </div> </template> <script> import Lifecycle21 from '@/components/LifeCycle2-1.vue' import Lifecycle22 from '@/components/LifeCycle2-2.vue' export default { name: 'Lifecycle2', components: { Lifecycle21, Lifecycle22 }, data: function() { return { selectedComponent: '', } }, beforeUpdate: function() { console.log('[LifeCycle2] beforeUpdate.') }, updated: function() { console.log('[LifeCycle2] updated.') }, beforeDestroy: function() { console.log('[LifeCycle2] beforeDestroy.') }, destroyed: function() { console.log('[LifeCycle2] destroyed.') }, errorCaptured: function() { console.log('[LifeCycle2] errorCaptured.') }, } </script> // スタイルは割愛
子コンポーネント1( LifeCycle21 )
<template> <div :class="$style.lifecycle2_child"> <p>input on LifeCycle21.</p> <input :class="$style.message" v-model="properties.message" placeholder="edit me"> <p>Message is: {{ properties.message }}</p> </div> </template> <script> export default { name: 'LifeCycle21', data: function() { return { properties: { message: 'default value.', }, } }, beforeDestroy: function() { console.log('[LifeCycle2-1] beforeDestroy.') }, destroyed: function() { console.log('[LifeCycle2-1] destroyed.') }, activated: function() { console.log('[LifeCycle2-1] activated.') }, deactivated: function() { console.log('[LifeCycle2-1] deactivated.') }, } </script> // スタイルは割愛
子コンポーネント2( LifeCycle22 )
<template> <div :class="$style.lifecycle2_child"> <p>input on LifeCycle22.</p> <input :class="$style.message" v-model="properties.message" placeholder="edit me"> <p>Message is: {{ properties.message }}</p> </div> </template> <script> export default { name: 'LifeCycle22', data: function() { return { properties: { message: 'default value.', }, } }, beforeDestroy: function() { console.log('[LifeCycle2-2] beforeDestroy.') }, destroyed: function() { console.log('[LifeCycle2-2] destroyed.') }, activated: function() { console.log('[LifeCycle2-2] activated.') }, deactivated: function() { console.log('[LifeCycle2-2] deactivated.') }, } </script> // スタイルは割愛
動作確認
初期表示( LifeCycle2 を表示 )
- 本稿のコードでは
beforeCreate
,created
のライフサイクルフックを設けていないので、ログには何も出力されない
LifeCycle21 を選択
LifeCycle21
のactivated
がログにでている- このとき親コンポーネントの
beforeUpdate
とupdated
で囲まれていることがわかる - つまり 動的コンポーネントが生成される イコール 親コンポーネントのDOMが更新される ということが、これでわかる
- このとき親コンポーネントの
LifeCycle21 でデータ入力
- データ入力した際、ログには何も出力されていない
- 子コンポーネントの方で
beforeUpdate
とupdated
のライフサイクルフックを設けていないので、子コンポーネントのログがでていないのは当たり前 - 親コンポーネントの方でもログが出ていないのが面白い
- 子コンポーネントで入力された情報は、あくまで子コンポーネント内で閉じた動き であり、そこで DOM が更新されても 親コンポーネントに影響しない ことがわかる
- 子コンポーネントの方で
LifeCycle22 を選択
- 子コンポーネントを切り替えたことで、親コンポーネントの方で
beforeUpdate
とupdated
が走った - コンポーネントが切り替えられたことで、
LifeCycle21
の方では 非活性 になるためdeactivate
が走っている - 逆に
LifeCycle22
は 活性 になるためactivate
が走っている
LifeCycle21 を再度選択
- 本動作確認におけるキモがこちら
LifeCycle22
>>LifeCycle21
に戻ったとき、 前に入力していた文字列が残っている- つまり
<keep-alive>
で動的コンポーネントを囲むことで、 囲まれたコンポーネントは切り替えが行われてもインスタンスの破棄は行われず、また インスタンスで保持していた値もそのまま残る、ということがこれで確認できた
画面上部のメニューから別ページを選択
- では動的コンポーネントの切り替えではなく、親コンポーネントごと別のコンポーネントに切り替えたらどうなるか
- ログからは追える実行順序が興味深い
- 親コンポーネントの
beforeDestroy
が走り - 選択していた
LifeCycle21
のdeactivate
が走る. - そして、同じく
LifeCycle21
のbeforeDestroy
とdestroyed
が走る - 次にもう一つの子コンポーネントである
LifeCycle22
のbeforeDestroy
とdestroyed
が走り - 最後に親コンポーネントの
destroyed
が走っている
- 親コンポーネントの
LifeCycle2 に戻る >> LifeCycle21 を選択
- 移動先のページから再度
LifeCycle2
を表示したときの動きを確認する- ログの出方は本動作確認の 手順-2 と同じ
- 注目したいのは 入力欄の値がデフォルト値になっている こと
- 一つ前の手順で確認したログからもわかるが、 親コンポーネントごと別のコンポーネントに切り替えると、
<keep-alive>
で囲っていても、子コンポーネントのインスタンスも破棄される ということが確認できた
動的コンポーネントを <keep-alive>
で囲まなかったら
上記は <keep-alive>
で動的コンポーネントを囲ったときの動きで、期待通り activate
と deactivate
が走ることが確認できた。
では動的コンポーネントを <keep-alive>
で囲まなかったときにどう動くのかを見ておきたい。
なお本項では親コンポーネントの <template>
をいじるだけで、子コンポーネントの変更はないので、子コンポーネントのコードは割愛する。
親コンポーネント( LifeCycle2 )
動的コンポーネントを <keep-alive>
で囲まなかったときにどう動くのか ?
<template> <div :class="$style.lifecycle2"> <-- ******** --> <-- 差分は割愛 --> <-- ******** --> <component :is="selectedComponent" :class="$style.lifecycle2_child_area"/> </div> </template> // テンプレートの差分だけなのでスクリプトは割愛 // スタイルは割愛
動作確認
初期表示( LifeCycle2 を表示 )
- 本稿のコードでは
beforeCreate
,created
のライフサイクルフックを設けていないので、ログには何も出力されない
LifeCycle21 を選択 >> LifeCycle21 でデータ入力
- 子コンポーネントである
LifeCycle21
でactivated
のログが出ていない - このことから、
<keep-alive>
で囲むことで、activate
が有効になることがわかる
LifeCycle22 を選択
- 親コンポーネントでの
beforeUpdate
とupdated
のログは出ているが、子コンポーネントであるLifeCycle21
でdeactivated
のログが出ていない - 前手順と同じく、ここでも
<keep-alive>
で囲むことで、deactivate
が有効になることがわかる
LifeCycle21 を再選択
- 親コンポーネントでの
beforeUpdate
とupdated
のログは出ているが、子コンポーネントであるLifeCycle21
でactivated
のログが出ていない - また
LifeCycle22
を選択する前に入力していた文字列が デフォルト文字列 に置き換わっている
ということで、
- 動的コンポーネントを
<keep-alive>
で囲まないと、コンポーネントの切り替えが行われたときに コンポーネントの破棄と生成 が行われること
が確認できた。 逆説的にいうと、
- 動的コンポーネントを
<keep-alive>
で囲むことでactivate
とdeactivate
がフックされること - またそうすることで、動的コンポーネントは別コンポーネントに切り替わってもインスタンスは非活性になるだけで生き続けること
- 再度もとのコンポーネントに切り替わることで、非活性 >> 活性に戻ること
と言うことができる。
errorCaptured
errorCaptured
についても、公式ページから説明を引用させていただく。
任意の子孫コンポーネントからエラーが捕捉されるときに呼び出されます。フックは、エラー、エラーをトリガするコンポーネントインスタンス、そしてどこでエラーが捕捉されたかの文字列情報、これら 3 つの引数を受け取ります。フックはエラーがさらにもっと伝播するのを防ぐために、false を返すことができます。
ということで、前傾のコードをベースにしつつ、こちらも動きを見ていく。
親コンポーネント( LifeCycle21 )
// // スクリプトの差分だけなのでテンプレートは割愛 // <script> export default { name: 'LifeCycle2', // // 差分のみ抜粋 // errorCaptured: function(error, component, info) { console.log('[LifeCycle2] errorCaptured.') console.log(error) console.log(component) console.log(info) // エラーの伝播を防ぐために、`false` を返す // つまり更に上の親へエラーをあげることはせずに、ここで止める return false }, } </script> // スタイルは割愛
子コンポーネント( LifeCycle21 )
どちらか片方のコンポーネントでエラーを発生させれば、errorCaptured
のフックは確認できるので、今回は LifeCycle21
の方でエラーを発生させた。
// // スクリプトの差分だけなのでテンプレートは割愛 // <script> export default { name: 'LifeCycle21', // // 差分のみ抜粋 // beforeCreate: function() { // わざと例外を発生させて、 // 親コンポーネントである LifeCycle2.vue の `errorCaptured()` のフックに引っ掛けたい this.properties.message = 'set on beforeCreate.' }, } </script> // スタイルは割愛
動作確認
初期表示( LifeCycle2 を表示 )
- 本稿のコードでは
beforeCreate
,created
のライフサイクルフックを設けていないので、ログには何も出力されない。
LifeCycle21 を選択してエラーを発生させる
LifeCycle21
を選択したことで、当該コンポーネントのインスタンスが動的に生成された- そのタイミングで子コンポーネントである
LifeCycle21P
でbeforeCreate
のフックが走り、例外が発生する - 発生した例外をフックして、親コンポーネントである
LifeCycle2
でerrorCaptured
が走った
errorCaptured
は 3 つの引数から以下の情報を得ることができる。
- 例外情報
- 例外が発生したコンポーネント情報
- 例外が発生し場所に関する情報を含む文字列
上記を踏まえ、改めてログを見てみると、先に上げた例外発生時の動きと errorCaptured
の引数から得た情報が合致していることがわかる。
まとめ
activate と deactivate
子コンポーネントを動的コンポーネントとして扱い、
<keep-alive>
で囲むことでactivate
とdeactivate
のフックが実行される<keep-alive>
で囲まれた子コンポーネントは、コンポーネントの切り替えが発生してもインスタンスは 非活性になるだけ で 破棄されずに生き残る- でも 親コンポーネントごと別のコンポーネントに切り替えられたら
<keep-alive>
で囲んでいても子コンポーネントのインスタンスは破棄される - 動的コンポーネントを
<keep-alive>
で囲まなかったら、通常のインスタンスと同じで コンポーネントの切り替え で インスタンスの生成と破棄 が行われる
errorCaptured
- 子コンポーネントでエラー が発生したら 親コンポーネント側 のこのフックが走る
errorCaptured
は 3 つの引数から以下の情報を得ることができる。- 例外情報
- 例外が発生したコンポーネント情報
- 例外が発生し場所に関する情報を含む文字列
false
で復帰することでエラーがさらにもっと伝播するのを防ぐことができる
ソースコード
今回の記事で動作確認に使用したコードは下記にアップしております。 ご参考まで。( 以下は ブランチのリンクですが、 master にもマージ済みです )
参考
公式
注意書き
本記事は Qiita でも公開 しております。
-
Vue.js のコンポーネントを単独のファイルとして作成する機能
拡張子「.vue」のファイルのことで<template>
,<script>
,<style>
のブロックで構成されている。↩