がんばってEventBusがイベントを複数回発火させてしまうのを食い止める


どうしてこの記事を書くことになったのか

ページ送り機能があるアプリの中で、共通ヘッダーのコンポーネントにあるページ送りボタンから、コンテンツ部分のコンポーネントにあるページ送り+APIを呼ぶメソッドを叩く実装がありました。
そこで、以下のような事象が発生しました

  • APIが複数回飛んでいる
  • ページも二つ先、三つ先のページに飛んでしまう

なんでこうなっちゃうの! というのを解消した記録です。
なお、ここで触れているEventBusはVue3.x系では消滅しました。

前提

  • node.js v14.15.5
  • vue.js v2.6.12
  • vue/cli 4.5.7
コード(ざっくり)
app.vue
<template>
  <Header />
  <Content />
</template>

<script>
  import Header ...
  import Content ...
header.vue
<template>
  <button @click="prev"></button>
  <button @click="next"></button>
</template>

<script>
  method: {
    prev():{ EventBus.$emit('prev'); }
    next():{ EventBus.$emit('next'); }
</script>

content.vue
method: {
  created() {
    EventBus.$on('prev'); // 前のページへ行くイベントのリスナー
    EventBus.$on('next'); // 次のページへ行くイベントのリスナー
    this.getData();
  },
  prev(): {
    // postAPIを叩く処理
    this.$router.push(prev);
  },
  next(): { 
    // postAPIを叩く処理
    this.$router.push(next);
    
  },
  getData(): { // getAPIを叩く処理 },
},
watch: {
  $route: { this.getDate(); },
}

原因調査

日本語で検索しても情報が少なそうだったので、EventBus dupulicateとかで検索してました。
引っかかったのがこのStack Overflow。

他にも似たような質問はいくつかあり、EventBusを使うとまあまあ起こる現象のようです。
こちらをもとに検証してみたところ、今回私が直面した事象は、前のページのコンポーネントのイベントリスナーが生きていて、そちらでもメソッドが実行されているということのようでした。

解消方法

結論としては、以下の4行をコンテンツ部分のコンポーネントに入れて解消しました。
コンポーネントが破棄される直前にEventBusのイベントリッスンを止めるようにする、ということですね。

  beforeDestroy() {
    EventBus.$off('prev'); // 前のページへ行くイベントのリスナー
    EventBus.$off('next'); // 次のページへ行くイベントのリスナー
  },

原因の原因調査

というわけで解消はしたわけですが、疑問は残ります。
コンポーネントってページ送りごとに破棄されてるんじゃないの…?(実装上は画面パスが変わったときにgetAPIを叩いて表示データを上書きしている状態だが、beforeDestroyの時に$offして解決しておりページ送りをした際にdestroyとcreatedはされている)

という疑問に解消してくれるのが以下の記事でした。(上記StackOverflowのコメントの中に紹介がありましたが、元のリンクは切れているようなので、Internet Archiveに残っていたものへのリンクを貼ります。)

こちらの記事によれば、

  • イベントバスのリスナーはグローバルに管理されているので、コンポーネントが作成されるたびに新しいリスナーが増えてしまう
  • それぞれのリスナーはイベントが発火したときに、元のコンポーネントを再生成する。(読解に自信なし…状況的には合ってると思いますが)
  • 複数回実行を避けるためには、コンポーネントが破棄されるときに手動でイベントリスナーも破棄する必要がある
  • Vueの設計上、他のコンポーネントの状態は知ることができないのが基本なので、それを無理やりどうにかするような実装はやめよう

とのことです。
コンポーネントが破棄されたときにイベントリスナーが破棄されない疑問が解消されました。
ちょっとEventBusの使い方を学んだだけでこの仕様を理解するのは無理があるように思いますが、そもそもEventBusを使うこと自体がVueの設計思想上のイレギュラーであると思うとやむなしのように思います。
(Vue3.x系で消されたのも納得…)

まとめ

  • EventBusのリスナーはグローバルに管理されているので、コンポーネントを破棄しただけでは破棄されず、手動で破棄する必要がある
  • 忘れずにEventBus.$off('hoge')
  • 賢い人「最初からVuexを使って実装すればよかったのでは?」 その通りだと思います…