既存コードへの Bacon.js 導入サンプル

適当なサンプルを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);

rangeFixmoveSlide といったクロージャ的な変数の参照や副作用をもつ関数は、高階関数や compose を多用すれば、さらに分解することは可能なはず。とはいえ、JavaScriptらしさを残そうと思うとこれくらいの力加減でも十分だとは思われる

Bacon.js 的なこと

EventStream はあるイベントが発生するたびに、そのイベントが発生したということのシグナルを投げ続ける。例えば Bacon.fromEventTarget(el, 'click') で取得できる EventStream は、el がクリックされるたびにシグナルが流される。EventStreamは無限リストとして表現され、mapfilter のような配列操作っぽいメソッドでフィルタされたりデータを割り当てられたりする。

配列を操作するときは一度にイテレーションされるが、EventStream だと多くの場合では散発的に発生したイベントをちまちまと処理することになる。なんにせよ、ひとつの引数を受け取りひとつの結果を返すシンプルな関数を用意しておくとちょうどいい。

Property は EventStream から生成される。scan()toProperty() を利用して、イベントストリームから現在の値を取り出す...っていう説明が多いが、なんかアレ。EventStream がそのときどきで使い捨てのように値を垂れ流すが、Property は値の保持を行って再利用しやすくしてくれる仕組みといえばいいかもしれない。

最終サンプル

上のコードとほぼ同じはずだけど、一応動作サンプル。

See the Pen azLZma by Ayumu Sato (@ahomu) on CodePen.

Bacon.jsだけで組むのはカロリー高そうだが、決まり切ったデータの流れをプロジェクト固有のラッパーレイヤーでサポートして、他のライブラリと組み合わせたら悪くない構成にできるかも。

参考