JavaScript
riot
riot.js

Riot.jsの落とし穴まとめ

More than 1 year has passed since last update.


はじめに

軽量かつ学習コストも低めで書きやすいライブラリの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は本当に素晴らしいライブラリなので、ぜひ使ってみてください♪