クマのブログ

つまづいたところ、学びを書いていきます

ライフサイクルフックの違いによるレンダリングのタイミング~img src編①~

はじめに

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な時

f:id:kuma_kuma0121:20220223214525p:plain

NGな時

f:id:kuma_kuma0121:20220223214455p:plain

ソースコード

親コンポ―ネント

<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>でやっていることは

  • コンポーネントから渡されたurlpropsプロパティで受け取る
  • 挙動を比較確認するためにbeforeCreate, created, beforeMoute, mounted, methods(getUrl)を各々定義
    • 手順としてはコメントアウトしてある箇所を随時コメントインして実行していく

デフォルト

ブラウザ

f:id:kuma_kuma0121:20220223214455p:plain

コンソール

f:id:kuma_kuma0121:20220223214818p:plain

エラーが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);
},

ブラウザ

f:id:kuma_kuma0121:20220223214525p:plain

コンソール

f:id:kuma_kuma0121:20220223214932p:plain

個人的には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);
},

コンソール(再インスタンス化)

f:id:kuma_kuma0121:20220223215015p:plain

設定済みの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と同じ

コンソール

f:id:kuma_kuma0121:20220223215100p:plain

ここも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>

ブラウザ

f:id:kuma_kuma0121:20220223214455p:plain

まさかのno-image表示

コンソール

f:id:kuma_kuma0121:20220223215147p:plain

なので、各ライフサイクルフックでも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画像表示の際)

f:id:kuma_kuma0121:20220223215244p:plain

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プロパティだったら…というパターンで別途同じ検証する