はじめに
軽量かつ学習コストも低めで書きやすいライブラリのRiot.jsですが、いわゆる落とし穴がいろいろあります。が、このライブラリに関する__日本語の__記事があまり多くなく、コード書いていると突然「あれっ!?」となることがたまにあるので、自分が知っているものを書いていきます。
※執筆現時点でのバージョンは2.4.1
です。
※2016/11/10追記
既に3.0.0-alpha.13
がリリースされた今、2.4.1
なんて古いバージョンを使っている方はいないと思いますので、今の時点での2系の最新である2.6.7
でも確認しました。
親タグマウント時に子タグもマウントされる
タグ(.tag
のこと)がネストしていると、親タグをマウントすると子タグも一緒にマウントされます。
例えば以下の様な場合、親タグ(app
タグ)で何か処理する必要があって、その結果でマウントする子タグを決めたい場合、子タグ(child-*
タグのいずれか)は2回マウントされてしまいます。以下のコード例のマウントデモ。
<!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>
<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
イベントは__クリックしても発火しません!__ (デモ)
<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
は本当に素晴らしいライブラリなので、ぜひ使ってみてください♪