既存コードへの Bacon.js 導入サンプル
Posted: Updated:
適当なサンプルをBacon.jsにしてみた
こんな感じの、ボタンを押したらスライドが切り替わるだけのサンプルを用意して、Bacon.js に無理のない範囲での書き換えを試みてみた。ご時世柄、jQueryは未使用。
See the Pen raGLMd by Ayumu Sato (@ahomu) on CodePen.
生のJavaScriptからスタート
どうせ Bacon.js で置き換えることになるので、サンプル時点でも構造化しないで手続きをダラダラと書いておいた。
document.addEventListener('DOMContentLoaded', function() {
getById('prev').addEventListener('click', function() {
moveSlide(-1);
});
getById('next').addEventListener('click', function() {
moveSlide(1);
});
var slides = toArray(document.querySelectorAll('#slides section'));
var start = 1;
var end = slides.length;
var current = 1;
function moveSlide(delta) {
var next = current + delta;
// current を start 以上 end 以下 に収める処理(読みづらいネー)
current = Math.min(end, Math.max(next, start));
slides.forEach(function(el) {
el.classList.remove('visible');
});
slides[current - 1].classList.add('visible');
}
}, false);
function getById(ident) {
return document.getElementById(ident);
}
function toArray(list) {
return Array.prototype.slice.call(list);
}
Bacon.jsで配線化してみる
Bacon.js では EventStream と Property が基本的なデータ操作の単位をあらわす。そのあたりの補足は後述するとして、大ざっぱに Bacon.js を導入してみると次のように表現できる。
従来の「〜〜をしたら、〜〜をする」というようなコードを「〜〜した」「〜〜の値」という細切れの単位で分解して、うまい具合に配線し直して動作を構成するようなイメージ。
document.addEventListener('DOMContentLoaded', function() {
var slides = toArray(document.querySelectorAll('#slides section'));
var start = 1;
var end = slides.length;
// UIの click イベントを EventStream に変換
var prev = Bacon.fromEventTarget(getById('prev'), 'click');
var next = Bacon.fromEventTarget(getById('next'), 'click');
// prev なら -1 という値を、next なら +1 という値を返す
// さらに 1本の EventStream にマージする
var both = prev.map(-1).merge(next.map(1));
// 現在値にやってきた値 v を足して、範囲内に収めてから返す
// scan(初期値, 現在値を更新する関数)
var current = both.scan(1, function(current, v) {
return rangeFix(add(current, v))
});
// 現在のページ番号変更を、スライド関数に渡す
current.onValue(moveSlide);
// 足すだけのアキュムレータ
function add(x, y) {
return x + y;
}
// 範囲内に収める関数(startとendは外からもらってる)
function rangeFix(z) {
return Math.min(end, Math.max(z, start));
}
// 状態を考慮せず、指定したページに移動できるように変更
function moveSlide(page) {
slides.forEach(function(el) {
el.classList.remove('visible');
});
slides[page - 1].classList.add('visible');
}
}, false);
rangeFix や moveSlide といったクロージャ的な変数の参照や副作用をもつ関数は、高階関数や compose を多用すれば、さらに分解することは可能なはず。とはいえ、JavaScriptらしさを残そうと思うとこれくらいの力加減でも十分だとは思われる
Bacon.js 的なこと
EventStream はあるイベントが発生するたびに、そのイベントが発生したということのシグナルを投げ続ける。例えば Bacon.fromEventTarget(el, 'click') で取得できる EventStream は、el がクリックされるたびにシグナルが流される。EventStreamは無限リストとして表現され、map や filter のような配列操作っぽいメソッドでフィルタされたりデータを割り当てられたりする。
配列を操作するときは一度にイテレーションされるが、EventStream だと多くの場合では散発的に発生したイベントをちまちまと処理することになる。なんにせよ、ひとつの引数を受け取りひとつの結果を返すシンプルな関数を用意しておくとちょうどいい。
Property は EventStream から生成される。scan() か toProperty() を利用して、イベントストリームから現在の値を取り出す...っていう説明が多いが、なんかアレ。EventStream がそのときどきで使い捨てのように値を垂れ流すが、Property は値の保持を行って再利用しやすくしてくれる仕組みといえばいいかもしれない。
最終サンプル
上のコードとほぼ同じはずだけど、一応動作サンプル。
See the Pen azLZma by Ayumu Sato (@ahomu) on CodePen.
Bacon.jsだけで組むのはカロリー高そうだが、決まり切ったデータの流れをプロジェクト固有のラッパーレイヤーでサポートして、他のライブラリと組み合わせたら悪くない構成にできるかも。
参考
- bacon.js の README の翻訳 (原文は2013年7月1日時点のものを取得)。最新の内容は https://github.com/raimohanska/bacon.js を参照。ライセンスは原文と同じです。
- ( programming -> girl -> ? ) | 業務アプリ実装にFRP使ってみた
- Start FRP