はじめに
Vueのライフサイクルフックの理解が曖昧だった。
特にコンポーネント間でprops
を使った、ページが描画されてすぐのcreated, mounted...あたりの理解が曖昧
以下のようなコードを見た時に
「どのタイミングで」
「どのようにurl
プロパティが変化しているのか」
が本当に理解しておらず結構ハマった(表示されるsrc属性の値はいつ ”” / url / transparentImageUrl / img/hoge.png のどれかになるんだ?)
↓親コンポーネント
<template> <parent-component :url="images.url" /> </template>
↓子コンポ―ネント
<template> <img v-if="url" src="url" > <img v-else :src="transparentImageUrl" > </template> <script> export default { props: { url: { type: String, default: "", }, }, data() { return { transparentImageUrl: image.TRANSPARENT_IMAGE_URL, }; }, created() { this.url = 'img/hoge.png'; }, ... } </script>
いまだに画面描画時に処理をしたい時にbeforeCreate, created, beforeMount, mountedの内どれを使えばいいのかの判断も曖昧なので、そんな自分の一助になればと思った
加えてmethodsでsrcの値をreturnするパターンはどうなのか、ということも確認
話すこと
以下コードを例にどのライフサイクルフックをつかったら、どんな挙動を示すのかを順にみていく
Vue公式ドキュメントのライフサイクルダイアグラムの図と一緒に見ていければと思う
やりたいこと
子コンポーネントで定義するimgタグのsrc属性の内容に応じて表示する画像を変更する
OKな時
NGな時
ソースコード
親コンポ―ネント
<template> <div> <ImageChildComponent :url="url" @replace="replaceUrl" ></ImageChildComponent> </div> </template> <script> import ImageChildComponent from './ImageChildComponent.vue' export default { components: { ImageChildComponent }, data() { return { url: '', } }, methods: { replaceUrl(newUrl) { console.log(newUrl); this.url = newUrl; }, } } </script>
<template>
でやっていることは
<script>
でやっていることは
子コンポーネント
<template> <div> <!-- OK時の画像 --> <div v-if="url" > <img :src="url" width=200 > <h2>OK!!!</h2> </div> <!-- NG時の画像 --> <div v-else > <img src="../assets/no-image.png" width=200 > <h2>NG...</h2> </div> </div> </template> <script> export default { props: { url: { type: String, default: "", }, }, beforeCreate() { // console.log('beforeCreate: ', this.url); // const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; // this.$emit("replace", newUrl); }, created() { // console.log('created: ', this.url); // const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; // this.$emit("replace", newUrl); }, beforeMount() { // console.log('beforeMount: ', this.url); // const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; // this.$emit("replace", newUrl); }, mounted() { // console.log('mounted: ', this.url); // const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; // this.$emit("replace", newUrl); }, methods: { getUrl() { // console.log('methods(getUrl): ', this.url); // const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; // this.$emit("replace", newUrl); return this.url; } } } </script>
<template>
でやっていることは
- 親コンポーネントから受け取った
url
の有無によって、表示内容を変更- 成功時:色付きの画像+「OK!!!」の文字列
- 失敗時:no-image画像+「NG...」の文字列
<script>
でやっていることは
- 親コンポーネントから渡された
url
をprops
プロパティで受け取る - 挙動を比較確認するために
beforeCreate
,created
,beforeMoute
,mounted
,methods(getUrl)
を各々定義- 手順としてはコメントアウトしてある箇所を随時コメントインして実行していく
デフォルト
ブラウザ
コンソール
エラーが2つ
[Vue warn]: Error in beforeCreate hook: "TypeError: Cannot read properties of undefined (reading 'url')”
TypeError: Cannot read properties of undefined (reading 'url')
- インスタンス化する前にthis.urlを参照しに行っているので、「urlなんてプロパティは存在しない!」って怒られている
beforeCreated
... beforeCreate() { console.log('beforeCreate: ', this.url); // 以下2行をコメントイン const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; this.$emit("replace", newUrl); }, ...
ブラウザ
デフォルトと同じなので割愛
コンソール
コチラもデフォルトと同じなので割愛
beforeCreateではインスタンスは生成しているがデータの初期化ができていないので、data()やpropsが参照できない
なのでデータ初期化していない(url = “” すらやっていない)状況でプロパティ参照しようが、urlにnewUrlを設定しようが意味ないよってこと
※beforeCreatedに console.log('beforeCreate: ', this.url); を書いていると常にエラーがでてくるので、これ以降はこの処理をコメントアウトする
created
created() { console.log('created: ', this.url); const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; this.$emit("replace", newUrl); },
ブラウザ
コンソール
個人的には3行目の@replaceイベント発火させた後のthis.urlに@replaceしたnewUrlが入っていないのが意外だった
なので、そのままcreated以降のライフサイクルフック内のthis.urlもcreatedで@replaceした値が反映されてない
ただ、再インスタンス化する(今回は以下のようにconsole.logをcreatedに追加)と、、、
created() { console.log('created: ', this.url); const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; this.$emit("replace", newUrl); // インスタンス化するために console.log('created: ', this.url); },
コンソール(再インスタンス化)
設定済みのurlが全てのライフサイクルフックで参照できるようになる
ギモン
ここはどういう流れなんだろう。。。
特に、初回読み込み時はブラウザ上ではnewUrlが表示されているのにもかかわらず、createdでも他のライフサイクルフックでもthis.urlがnewUrlを参照しておらず、nullだったのはまだ理解ができない。。。
createdの動きについてつかみ切れていない…
beforeMount
beforeMount() { console.log('beforeMount: ', this.url); const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; this.$emit("replace", newUrl); },
ブラウザ
createdと同じ
コンソール
ここもcreatedと同じく、beforeMounteを通った以降はthis.urlがnewUrlを参照していないんだが、ブラウザの表示はnewUrlになっている、という状況
ギモン
createdと同じ
mounted
mounted() { console.log('mounted: ', this.url); const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; this.$emit("replace", newUrl); },
ブラウザ
created, beforeMouteと同じ
コンソール
created, beforeMouteと同じ
ギモン
created, beforeMouteと同じ
methods
ここで想定するのは以下のようなソースコード
<template> <div v-if="url" > <h2>OK!!!</h2> <img :src="getUrl()" // 変更 width=200 > </div> ... </template> <script> export default { ..., methods: { getUrl() { console.log('methods(getUrl): ', this.url); const newUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='; this.$emit("replace", newUrl); return this.url; }, ... } </script>
ブラウザ
まさかのno-image表示
コンソール
なので、各ライフサイクルフックでもthis.urlの値はnull
getUrl内にあるconsole.log('methods(getUrl): ', this.url);も実行されていない
これに関しては流れとしては以下の通り
1, 親→子にurlを渡され、v-if=”url”を評価。親から渡されるurlはnullなので、v-else側に処理が入る
2, 結果、no-imageが表示され、各ライフサイクルフックでもthis.urlはnullとなる
3, さらにgetUrlメソッドが呼び出されないため、console.log('methods(getUrl): ', this.url);も実行されない
※getUrlを呼び出すにはv-if="url”をv-if="getUrl”にすれば、OK画像の表示ができる
<div v-if="getUrl()" <!-- 変更 --> > <h2>OK!!!</h2> <img :src="getUrl()" width=200 > </div>
コンソール(OK画像表示の際)
v-if時と:srcの値設定の際にgetUrlが3回呼び出されるので、見ずらい
気になったのは3行目でmountedの前にgetUrlメソッドが呼ばれ、処理を実行してから、mountedに入っている点がライフサイクルフックの通りだなと実感できた
3回呼ばれている内訳は以下推測
- 1回目:初回インスタンス時、v-if内のgetUrlメソッドのみ実行
- 2,3回目:templateが描画される際にv-ifと:srcのgetUrlが呼び出される
現状ではすぐわからないため、仮説を立て、情報を当てつつ正解を見つけていく
参考記事
https://jp.vuejs.org/v2/guide/instance.html#ライフサイクルダイアグラム
https://qiita.com/chan_kaku/items/7f3233053b0e209ef355
https://qiita.com/ksh-fthr/items/2a9f173c706ef6939f93
ギモン
- re-renderすることでbeforeMountとmountedが呼ばれるが、re-renderのトリガーになる事象は何なのか気になった
- 推測としてはdataやpropsの内容に変化が起こった時にre-renderされると推測
- src属性が子コンポーネント特有dataプロパティだったら…というパターンで別途同じ検証する