👏
宣言的UIなJSフレームワーク ベンチマークコード
JavaScriptフレームワークの有名なベンチマークをコーディングしてみる
ベンチマークの内容
- 1000行のリスト生成・表示
- 1000行のリスト置換・表示
- 10000行のリスト生成・表示
- 1000行のリスト追加・表示
- 10行毎に行更新
- リストクリア
- 行の内容を交換
- 行選択
- 行削除
状態の保持
- ViewModelクラスのプロパティとして宣言
- リストと選択行のidを保持
main.js
data = []; // リスト
selected; // 選択行のid
行の選択状態
- アクセサプロパティ(getter)で行の選択状態を定義
- 行のidと選択行idが一致する場合、選択中を返す
- ループ中のアクセサプロパティでは、ワイルドカードを使った
this["data.*.id"]
へアクセスできる - アクセサプロパティ(getter)を使う場合、依存関係の定義を行う
main.js
get "data.*.selected"() {
return this["data.*.id"] === this.selected;
}
// 依存関係の定義
$dependentProps = {
"data.*.selected": [ "data.*.id", "selected" ],
}
各処理
- ViewModelクラスのメソッドとして実装
1000行のリスト生成・表示
- 1000行データを作成して、プロパティを更新
- 選択行idをクリア
main.js
run() {
this.data = buildData(1000);
this.selected = undefined;
}
10000行のリスト生成・表示
- 10000行データを作成して、プロパティを更新
- 選択行idをクリア
main.js
runLots() {
this.data = buildData(10000);
this.selected = undefined;
}
1000行のリスト追加・表示
- 1000行データを作成し追加、プロパティを更新
main.js
add() {
this.data = this.data.concat(buildData(1000));
}
10行毎に行更新
- 10行毎に、行データを更新
- プロパティの更新は、ドット記法のプロパティ名で行う
main.js
update() {
for(let i = 0; i < this.data.length; i += 10) {
this[`data.${i}.label`] += ` !!!`;
}
}
リストクリア
- 空の配列で、プロパティを更新
main.js
clear() {
this.data = [];
}
行の内容を交換
- 分割代入でスワップ処理を記述
- プロパティのアクセスは、ドット記法のプロパティ名で行う
main.js
swapRows() {
if (this.data.length > 998) {
[this["data.1"], this["data.998"]] = [this["data.998"], this["data.1"]];
}
}
行の選択
- 選択行idを更新
- ループ中のイベントハンドラは、ワイルドカードを使った
this["data.*.id"]
へアクセスできる
main.js
select() {
this.selected = this["data.*.id"];
}
行の削除
- 行をインデックスにより削除し、リストプロパティを更新
- ループ中のイベントハンドラでは、引数としてコンテキスト変数(インデックス値)
$1
を利用できる
main.js
remove(e, $1) {
this.data = this.data.toSpliced($1, 1);
}
Viewの定義
- html変数で定義
ボタンのイベント処理
- data-bind属性で、onclickとViewModelクラスのメソッドを関連付ける
html変数(必要な部分だけ抜粋)
<button id="run" data-bind="onclick:run">Create 1,000 rows</button>
<button id="runlots" data-bind="onclick:runLots">Create 10,000 rows</button>
<button id="add" data-bind="onclick:add">Append 1,000 rows</button>
<button id="update" data-bind="onclick:update">Update every 10th row</button>
<button id="clear" data-bind="onclick:clear">Clear</button>
<button id="swaprows" data-bind="onclick:swapRows">Swap Rows</button>
リスト
- 繰り返し(ループ)は、
loop:
~end:
で囲む - ループ対象のプロパティ
data
をloop:
の後ろに書く - ループ中の要素へのアクセスは、ワイルドカードを使ったドット記法で行う
data.*
- class属性へのクラス名の追加・削除は、
class.クラス名:プロパティ
で行う- プロパティが真の場合追加、偽の場合削除される
- ループ中のアクセサプロパティ
data.*.selected
では、コンテキスト変数が使用できる - ループ中のイベント
select()
remove()
では、コンテキスト変数が使用できる
html変数(必要な部分だけ抜粋)
<table>
<tbody>
{{ loop:data }}
<tr data-bind="class.danger:data.*.selected">
<td>{{ data.*.id }}</td>
<td><a data-bind="onclick:select">{{ data.*.label }}</a></td>
<td><a data-bind="onclick:remove"><span class="glyphicon-remove"></span></a></td>
<td></td>
</tr>
{{ end: }}
</tbody>
</table>
考察
- 他のフレームワークよりもわかりやすく簡潔に記述できたと思う
ファイル構成
--+-- index.html
|
+-- quel.min.js [フレームワーク]
|
+-- buildData.js [データ生成]
|
+-- main.js [コンポーネント定義]
ソースコード
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>quel-"keyed"</title>
<link href="/path/to/currentStyle.css" rel="stylesheet"/>
</head>
<body>
<myapp-main></myapp-main>
<script type="module">
import quel from "./quel.min.js";
import MyappMain from "./main.js";
quel.componentModules({ MyappMain });
</script>
</body>
</html>
buildData.js
// データ生成用
function random(max) {
return Math.round(Math.random() * 1000) % max;
}
const A = ["pretty", "large", "big", "small", "tall", "short", "long", "handsome", "plain", "quaint", "clean",
"elegant", "easy", "angry", "crazy", "helpful", "mushy", "odd", "unsightly", "adorable", "important", "inexpensive",
"cheap", "expensive", "fancy"];
const C = ["red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange"];
const N = ["table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", "mouse",
"keyboard"];
let nextId = 1;
export function buildData(count) {
const data = new Array(count);
for (let i = 0; i < count; i++) {
data[i] = {
id: nextId++,
label: `${A[random(A.length)]} ${C[random(C.length)]} ${N[random(N.length)]}`,
};
}
return data;
}
main.js
import { buildData } from "./buildData.js";
// Viewの定義
const html = `
<div class="container">
<div class="jumbotron">
<div class="row">
<div class="col-md-6">
<h1>quel keyed</h1>
</div>
<div class="col-md-6">
<div class="row">
<div class="col-sm-6 smallpad">
<button class="btn btn-primary btn-block" id="run" data-bind="onclick:run">Create 1,000 rows</button>
</div>
<div class="col-sm-6 smallpad">
<button class="btn btn-primary btn-block" id="runlots" data-bind="onclick:runLots">Create 10,000 rows</button>
</div>
<div class="col-sm-6 smallpad">
<button class="btn btn-primary btn-block" id="add" data-bind="onclick:add">Append 1,000 rows</button>
</div>
<div class="col-sm-6 smallpad">
<button class="btn btn-primary btn-block" id="update" data-bind="onclick:update">Update every 10th row</button>
</div>
<div class="col-sm-6 smallpad">
<button class="btn btn-primary btn-block" id="clear" data-bind="onclick:clear">Clear</button>
</div>
<div class="col-sm-6 smallpad">
<button class="btn btn-primary btn-block" id="swaprows" data-bind="onclick:swapRows">Swap Rows</button>
</div>
</div>
</div>
</div>
</div>
<table class="table table-hover table-striped test-data">
<tbody>
{{ loop:data }}
<tr data-bind="class.danger:data.*.selected">
<td class="col-md-1">{{ data.*.id }}</td>
<td class="col-md-4"><a data-bind="onclick:select">{{ data.*.label }}</a></td>
<td class="col-md-1"><a data-bind="onclick:remove"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a></td>
<td class="col-md-6"></td>
</tr>
{{ end: }}
</tbody>
</table>
</div>
<span class="preloadicon glyphicon glyphicon-remove" aria-hidden="true"></span>
`;
// ViewModelの定義
class ViewModel {
data = [];
selected;
get "data.*.selected"() {
return this["data.*.id"] === this.selected;
}
select() {
this.selected = this["data.*.id"];
}
remove(e, $1) {
this.data = this.data.toSpliced($1, 1);
}
run() {
this.data = buildData(1000);
this.selected = undefined;
}
runLots() {
this.data = buildData(10000);
this.selected = undefined;
}
add() {
this.data = this.data.concat(buildData(1000));
}
update() {
for(let i = 0; i < this.data.length; i += 10) {
this[`data.${i}.label`] += ` !!!`;
}
}
clear() {
this.data = [];
}
swapRows() {
if (this.data.length > 998) {
[this["data.1"], this["data.998"]] = [this["data.998"], this["data.1"]];
}
}
$dependentProps = {
"data.*.selected": [ "data.*.id", "selected" ],
}
}
export default { ViewModel, html };
Discussion