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

## 何が起きるのか

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

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

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

## 体験してみよう
### イベントを発する（publishする）ものを定義する

下記の`HumanStore`は`startLife`メソッドを実行すると、

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

```js
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を受け取るとコンソールにログ出力します。

```html
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>
```

### ルーティングする

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

```html
<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>
```

上記は省略しているので全文は[こちらのソースを参照](https://github.com/NewGyu/riot-sample/blob/obseriot/duplex-events/src/app.tag)してください。

### 動作を確認する

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

![localhost-8080-(iPhone 6 Plus).png](https://qiita-image-store.s3.amazonaws.com/0/31010/f870eaf6-3596-c79b-1064-e241a5c0d6da.png)

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

![Screenshot from 2017-07-10 22-08-43.png](https://qiita-image-store.s3.amazonaws.com/0/31010/48bcbc36-5673-2405-b3ac-12f9bd32b39b.png)

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

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

![Screenshot from 2017-07-10 22-09-36.png](https://qiita-image-store.s3.amazonaws.com/0/31010/4898e71a-83e3-9ba2-ed7e-848b04fa48cf.png)

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

## なぜこうなるのか

冒頭に述べたとおり、

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

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

## どうしたら良いのか

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

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