Node.js
riot.js
obseriot

Riot.jsでobservableなイベントハンドリングが複数回動作してしまう

More than 1 year has passed since last update.

何が起きるのか

Riot.jsの <script>...</script>はマウントされるたびに動くというところにハマって、注意しないとobservableなイベントを複数回処理してしまうよというお話です。

サンプルコードはこちらです。
https://github.com/NewGyu/riot-sample/tree/obseriot/duplex-events

本稿ではriot-observableを薄くラップしたobseriotというライブラリを使っていますが、この事象はobseriot固有のものではありません。Singletonなobservableオブジェクトを使うとハマりやすい問題です。

体験してみよう

イベントを発する(publishする)ものを定義する

下記のHumanStorestartLifeメソッドを実行すると、

  1. まずBornイベントをpublish
  2. その後1秒おきにLivingイベントをpublish
  3. そして5秒経つとDeadイベントをpublishして動きを止めます
import ob from "obseriot";

export const HumanStore = new class HumanStore {
  constructor() {

  }

  startLife() {
    ob.notify(HumanEvent.Born);
    const t = Date.now();
    const intervalId = setInterval(()=>{
      if(Date.now() > t + 5000) {
        clearInterval(intervalId);
        ob.notify(HumanEvent.Dead);
        return;
      }
      ob.notify(HumanEvent.Living);
    } , 1000);
  }
}

export const HumanEvent = {
  Born: {
    handler: {
      name: "human.born",
      action: () => {}
    }
  },
  Living: {
    handler: {
      name: "human.living",
      action: () => {}
    }    
  },
  Dead: {
    handler: {
      name: "human.dead",
      action: () => {}
    }
  }
}

イベントを受ける(subscribeする)タグを定義する

下記のpage1がmountされるとHumanStore.startLifeが実行されます。
そして、HumanEventを受け取るとコンソールにログ出力します。

import ob from "obseriot";
import {HumanStore, HumanEvent} from "../../lib/Human";

<page1>
  <p>男の人が額縁の絵を壁にかけようとしています。</p>
  <figure class="image">
    <image src="img/kaiga.png">
  </figure>

  <script>
    ob.listen(HumanEvent.Born, ()=>{
      console.log("He was born");
    });

    ob.listen(HumanEvent.Living, ()=>{
      console.log("He is living");
    });

    ob.listen(HumanEvent.Dead, ()=>{
      console.log("He was Dead");
    });

    this.on("mount", ()=>{
      HumanStore.startLife();
    });
  </script>
</page1>

ルーティングする

さて親コンポーネントで次のようなルーティングを設定します。

<app>
  <article class="section">
    <page class="container"></page>
  </article>

  <script>
    route("/page1", () => {
      this.activePage = "page1";
      this.subtitle = "絵のページ";
      this.update();
      riot.mount("page", "page1");
    });
    route("/page2", () => {
      this.activePage = "page2";
      this.subtitle = "音楽のページ";
      this.update();
      riot.mount("page", "page2");
    });
    route.start(true);
  </script>
</app>

上記は省略しているので全文はこちらのソースを参照してください。

動作を確認する

npm start して、 http://localhost:8080 にアクセスしてみます。

localhost-8080-(iPhone 6 Plus).png

上記のようなページが出るので、Chromeの開発者ツール等でconsole.log の様子を見てみましょう。

Screenshot from 2017-07-10 22-08-43.png

mountと同時にHumanStore.startLifeが実行されるのでこのようにログ出力されます。

さて、page2に切り替えて、もう一度page1に戻してみましょう。

Screenshot from 2017-07-10 22-09-36.png

なんだか2回ずつログが記録されていますね。
page1,page2を何度も何度も切り替えるとログ出力される回数がどんどん増えていきます。

なぜこうなるのか

冒頭に述べたとおり、

Riot.jsの <script>...</script>はマウントされるたびに動く

からなんですね。
マウントされるたびに ob.listenがcallされるので、イベントハンドラコールバックが複数登録されてしまうためです。

どうしたら良いのか

今のところunmount時にイベントハンドラコールバックの登録を解除することしか思いつきません。

this.on("unmount", ()=>{
  ob.remove(HumanEvent.Born);
  ob.remove(HumanEvent.Living);
  ob.remove(HumanEvent.Dead);
});