161
159

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Riot.jsの落とし穴まとめ

Last updated at Posted at 2016-07-08

はじめに

軽量かつ学習コストも低めで書きやすいライブラリのRiot.jsですが、いわゆる落とし穴がいろいろあります。が、このライブラリに関する__日本語の__記事があまり多くなく、コード書いていると突然「あれっ!?」となることがたまにあるので、自分が知っているものを書いていきます。

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

※2016/11/10追記

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

親タグマウント時に子タグもマウントされる

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

例えば以下の様な場合、親タグ(appタグ)で何か処理する必要があって、その結果でマウントする子タグを決めたい場合、子タグ(child-*タグのいずれか)は2回マウントされてしまいます。以下のコード例のマウントデモ

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>
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にボタンがいて、onclickhoge()というイベントハンドラが設定されているとします。このイベントハンドラは()が付いているため、appタグが__マウントされた時に実行__されます。さらに、実際に実行していただくとわかりますが、このボタンのhoge_eventイベントは__クリックしても発火しません!__ (デモ

<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

<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で表示されるだけです。(展開されないデモ

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

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

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

<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オブジェクトに固定する方法も良いですね。

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要素にイベントを付与しています)

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

計測結果はこんな感じになりました。(ベンチマークデモ)
※各ループ回数にて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メソッドによるループ速度を計測してみた

おわりに

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

161
159
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
161
159

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?