Riot.jsの落とし穴まとめ

# はじめに
軽量かつ学習コストも低めで書きやすいライブラリの[Riot.js](http://riotjs.com/ja/)ですが、いわゆる落とし穴がいろいろあります。が、このライブラリに関する__日本語の__記事があまり多くなく、コード書いていると突然「あれっ！？」となることがたまにあるので、自分が知っているものを書いていきます。

※執筆現時点でのバージョンは`2.4.1`です。

### ※2016/11/10追記
既に`3.0.0-alpha.13`がリリースされた今、`2.4.1`なんて古いバージョンを使っている方はいないと思いますので、今の時点での2系の最新である`2.6.7`でも確認しました。

## 親タグマウント時に子タグもマウントされる
タグ(`.tag`のこと)がネストしていると、親タグをマウントすると子タグも一緒にマウントされます。

例えば以下の様な場合、親タグ（`app`タグ）で何か処理する必要があって、その結果でマウントする子タグを決めたい場合、子タグ（`child-*`タグのいずれか）は２回マウントされてしまいます。以下のコード例の[マウントデモ](https://k-kuwahara.github.io/demo/riot/pitfalls/parent-childs/index.html)。

```html:html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8"/>
    <title>sample</title>
    <script src="/js/riot+compiler.min.js"></script>
  </head>

  <body>
    <!-- 親タグ -->
    <app></app>

    <!-- タグファイルのimport -->
    <script src="/tag/app.tag" type="riot/tag"></script>
    <script src="/tag/foo.tag" type="riot/tag"></script>
    <script src="/tag/bar.tag" type="riot/tag"></script>
    <script src="/tag/baz.tag" type="riot/tag"></script>

    <!-- 親タグのマウント -->
    <script>
      riot.mount('app', {title: 'hogehoge'});
    </script>
  </body>
</html>
```

```html:tag
<app>

  <!-- layout -->
  <h3>{ opts.title }</h3>

  <!-- 子タグ -->
  <foo></foo>
  <bar></bar>
  <baz></baz>

  <!-- javascript -->
  <script>
    this.on('mount', function() {

      // 何らかの処理後
      hoge = '2';

      // hogeの値によりマウントする子タグを指定
      switch(hoge) {
        case '1':
          riot.mount('foo');
          break;

        case '2':
          riot.mount('bar', {text: 'mount!!'});
          break;

        default:
          riot.mount('baz');
          break;
      }
    });
  </script>

</app>
```

それぞれの子タグ内でAPIを叩きデータを取得後、そのデータを加工してhtmlに表示していた場合、無駄に2回もAPIと通信してデータ加工することになってしまいます。


## `{ hoge_event() }`はマウント時のみ実行される

例えば以下のようにhtmlにボタンがいて、`onclick`に`hoge()`というイベントハンドラが設定されているとします。このイベントハンドラは`()`が付いているため、`app`タグが__マウントされた時に実行__されます。さらに、実際に実行していただくとわかりますが、このボタンの`hoge_event`イベントは__クリックしても発火しません！__ （[デモ](https://plnkr.co/edit/f3Mocc?p=preview)）

```html
<app>

  <!-- layout -->
  <h3>{ opts.title }</h3>

  <input type="button" value="click" onclick="{ hoge_event() }">

  <!-- javascript -->
  <script>
    hoge_event(e) {
      // 何らかの処理
    }
  </script>

</app>
```

これは、`hoge()　→　hoge`のように、`()`を抜けばマウント時に実行されません。


## `each`ループの中は`opts`変数の中は展開されない
これは特に説明はいらないですかねw

```html
<app>

  <!-- layout -->
  <h3>{ opts.title }</h3>

  <ul>
    <li each={ items }>
      <label>
        <input type="checkbox" name="hoge[]" value={ id }> { opts.name }
      </label>
    </li>
  </ul>

</app>
```

`{ opts.name }`のところが展開されず、上記のコードは単にチェックボックスが`li`で表示されるだけです。（[展開されないデモ](https://github.com/k-kuwahara/k-kuwahara.github.io/tree/master/demo/riot/pitfalls/deployment-opts)）

※追記
@atomita さんに、ループ内で`opts`変数の値の取得方法をコメントいただきました！
`parent.opts.name`のように`parent`から参照すれば取得できます。

## `this`が何を表すかに気をつける

タグ（`.tag`）の`script`タグ内の`this`は「マウントされたタグのオブジェクト」ですが、`JavaScript`固有のメソッド内であったり、素のJavaScriptのイベントハンドラ内ですと、`this`はコンテキストとなります。

```html
<app>

  <!-- layout -->
  <h3>{ opts.title }</h3>

  <div>
    <input type="button" id="hoge" value="click">
  </div>

  <!-- javascript -->
  <script>
    // ここはappオブジェクト


    this.on('mount', function() {
      // もちろんここもappオブジェクト

      var t = document.getElementById('hoge');

      t.addEventListener('click', function() {
        // ※ここは'#hoge'で指定された要素（オブジェクト）
        // なので、ここではRiot.jsの機能はthisでは使えない！

      });
    });
  </script>
</app>
```

これはRiot.jsというよりJavaScriptの文法ですね。
対応としては、`script`タグの頭で`var _this = this`などのように別の変数にappオブジェクトをコピーしておくと良いです。もしくは`bind()`を使って関数内のthisをappオブジェクトに固定する方法も良いですね。

```js
var t = document.getElementById('hoge');

t.addEventListener('click', function(ev) {
  // この中のthisもriotのオブジェクト！
  // targetについてはev.targetで取得する

}.bind(this)); // このthisはriotのオブジェクト
```


## `riot.unmount()`は使えない
`riot`オブジェクトには

- mount
- update
- route
- mixin
…etc

などのメソッドが定義されていますが、`unmount`は定義されていません。あ、じゃあ使えないんじゃん！って早合点してしまった自分ですが、公式サイトにちゃんと書いてありましたw

`unmount`が使えるのは「マウントされたタグのオブジェクト」でした。ですので、マウント後の`script`タグ内で`this.unmount()`の形で利用できます。


## 同じタグ名でマウントすると後勝ちとなる
@Takepepe さんにコメントいただきました（コメント欄参照）が、同じ名前のタグを何度か別々のファイルでマウントすると、後からマウントされたもので上書きされてしまいます。

- タグの命名規則を決める
- マウントする記述は一箇所、残りはアップデートイベントにする

などの対策をする必要がありますね。


## `each`ループは件数が多いと遅い（※追記あり）
`each`メソッドはとても便利ですが、ループの回数が多いとDOM生成に結構時間がかかります。以下はベンチマークの計測のコードです。ベンチマークの測り方は、セレクトボックスからループ回数を選択された瞬間から`updated`イベント発火までとなります。（簡単に各`li`要素にイベントを付与しています）

```html
<app>
  <h3>{ opts.title }</h3>

  <div id="container">

    <div id="select" class="box">
      <p>select loop times</p>
      <form onchange="{ view }">
        <select name="times">
          <option value="">please select</option>
          <option value="10">10</option>
          <option value="100">100</option>
          <option value="1000">1000</option>
          <option value="10000">10000</option>
        </select>
      </form>

      <ul if="{ items.length > 0 }">
        <label each="{ val, i in items }" onclick="{ toggle_bgcolor }">
          <li class="bg-off">
            <input type="checkbox" name="hoge[]"/>hoge{ i+1 }
          </li>
        </label>
      </ul>
    </div>

    <div id="result" if="{ items.length > 0 }" class="box">
      <p>
        <b>It took to mount time</b><br/>
        { view_time }(s)
      </p>
    </div>
  </div>

  <!-- javascript -->
  <script>
  // for benchmark
  this.view_time = 0
  // default items
  this.items = []
  // init start time
  this.start = 0
  // updated flg
  var updated_flg = true

  // view list
  view(e) {
    this.items = Array(parseInt(e.target.value))
    this.start = (new Date()).getTime()
    updated_flg = false
  }

  // after updated
  this.on('updated', function() {
    if (!updated_flg) {
      var end = (new Date()).getTime()
      this.view_time = (end - this.start) / 1000
      updated_flg = true
      // updated value to DOM
      this.update()
    }
  })

  // if checked, change background-color
  toggle_bgcolor(e) {
    if ($(e.target).find('input').prop('checked')) {
      $(e.target).addClass('bg-off')
      $(e.target).removeClass('bg-on')
      $(e.target).find('input').prop('checked', false)
    }
    else {
      $(e.target).addClass('bg-on')
      $(e.target).removeClass('bg-off')
      $(e.target).find('input').prop('checked', true)
    }
  }
  </script>
</app>
```

計測結果はこんな感じになりました。（[ベンチマークデモ](https://k-kuwahara.github.io/demo/riot/pitfalls/loop-benchmark/index.html))
※各ループ回数にて10回ずつ計測し、その平均時間になります。

||10回ループ|100回ループ|1000回ループ|10000回ループ|
|:---|---:|---:|---:|---:|
|1回目|0.018|0.064|0.686|6.538|
|2回目|0.022|0.057|0.467|6.65|
|3回目|0.015|0.056|0.455|6.276|
|4回目|0.012|0.057|0.444|6.229|
|5回目|0.027|0.078|0.537|5.88|
|6回目|0.015|0.083|0.381|6.682|
|7回目|0.011|0.05|0.457|6.629|
|8回目|0.012|0.057|0.53|6.669|
|9回目|0.021|0.055|0.386|6.725|
|10回目|0.028|0.063|0.441|5.706|
|__平均(s)__|__0.0181__|__0.062__|__0.4784__|__6.3984__|

1000件程度ではそれほど影響は出ないかなとは思いますが、数千を超えると…
また実際は

* CSS等で装飾をする
* イベントハンドラをセットする
* 条件によって表示/非表示

などの処理をするでしょうから、もう少し遅くなると思います。したがって一度にデータを表示するのではなく、スクロールされたらその時にAjaxで続きのデータを取ってくる、などの工夫が必要です。

### ※2017/09/26 追記
`v3.7.2`にて計測したものをブログに書きましたので、ご参考までに↓

[Riot.js（v3）のeachメソッドによるループ速度を計測してみた](http://kito0039.hatenablog.com/entry/2017/01/15/225304)

# おわりに
まだ自分も落ちたことがない落とし穴はあるかもしれませんので、この記事は随時更新します。また。`Riot.js`は本当に素晴らしいライブラリなので、ぜひ使ってみてください♪