Posted at

observe-jsを利用して分かりやすく非同期のFIFOを実装してみる

More than 3 years have passed since last update.


宣伝

最近 Dev:BOARD という物を作っています。

詳しくは この記事 を読んでもらうとし、その作成途中で「複数カラムに画像を含む高さがまちまちなカードを時系列順で順次追加していく」という処理が必要になりまして jQuery-RTile というjQuery拡張を作りました。

そちらについては こちらの記事 を読んでもらうとし、そこで利用した observe-js というライブラリが面白かったので紹介してみたいと思います。

ここまで出てきたリンクのまとめ

ということで宣伝はここまでにし、早速 observe-js の紹介に入りたいと思います。


OBSERVE-JS


observe-jsとは


Polymer/observe-js


A library for observing Arrays, Objects and PathValues


observe-js は Googleが 開発 しているPolymer用のライブラリのようです。

Array.observe() はES7に入るかもしれない感じですが、現時点でサポートしているブラウザはChrome36以上のみなのでこちらのライブラリを利用することにしました。

名前から分かる通り、ArrayやHashを監視して処理を入れることができるので簡単な非同期なキューイングシステムとしても利用できます。

簡単なキューイングシステムくらいであればこのライブラリを使う意味はあまり無いのですが、データの追加、データの実行、キューの管理が分離されて非常にわかりやすく作れます。

今回はFIFOとしての利用だったので ArrayObserver を利用しましたが、 ObjectOvserver を利用すればUIパーツなどの状態変化に利用できそうなのでそちらも時間を見て試してみたいところです。


FIFOとしての利用

出来るジャバスクリプターの人はこういうものを使わなくてもPromiseなど使って自分で処理したり、もっと良いライブラリを使ったりしてるでしょうから、ここではそのような人じゃない人をターゲットにサンプルを交えて紹介していきたいと思います。


前準備

分かりやすいサンプルのために以下のhtmlファイルを準備します。

<html>

<head>
</head>
<body>
<div id="queue"></div>
<script src="observe.js"></script>
<script src="script.js"></script>
</body>
</html>

次に同じフォルダにダウンロードして用意したobserve-jsと空のscript.jsを準備します。

そうそう、最近ローカルでの開発にはNode製の local-web-server を利用しています。

凝った使い方はせずに ws として起動させるだけなのですが、凝ったことをしないだけにシンプルに起動できるのがいい感じです。


キューへの追加

キューへの追加は Array.push() で追加していくだけです。

var items = [];

function add(i) {
setTimeout(function(i) {
items.push(i);
document.getElementById("queue").innerHTML = items.join(", ");
}, 5000 * Math.random(), i);
}
for (var i=0; i<20; i++) {
add(i);
}

これでキューへの追加と追加される度に追加された内容が画面に表示されるはずです。値はループで回した20個になるはずです。


キューされた処理の実行

次に追加された後の処理を加えます。

function run() {

setTimeout(function() {
console.log("run");
items.shift();
document.getElementById("queue").innerHTML = items.join(", ");
}, 5000);
}
run();

キューに加えたあとに実行させると、先ほどと同じように追加された内容が画面に表示されるはずです。ですが、先ほどと違い最後に run() で一度処理を走らせているため値の数は19個になるはずです。

また、コンソールログを見てみると数字の並びの中に run というメッセージがあることも確認出来ると思います。

この中で、items.shift() が次への処理のトリガー(まだトリガー後の処理は定義してません)になっているのですが見ての通り非同期に行われています。


observe-jsを利用しない方法

先にobserve-jsを使わずに処理する方法を書いておきます。

今までのコードを前提にすると以下の感じになるでしょうか。

var items = [];

function add(i) {
setTimeout(function(i) {
items.push(i);
if (items.length === 1) { run(); }
document.getElementById("queue").innerHTML = items.join(", ");
}, 5000 * Math.random(), i);
}

for (var i=0; i<20; i++) {
add(i);
}

function run() {
if (items.length === 0) { return; }
setTimeout(function() {
var item = items.shift();
run(item);
document.getElementById("queue").innerHTML = items.join(", ");
}, 1000);
}

まあ別に大変なところもありませんし、分かりづらくももありません。

ですが、キューに入れる箇所が複数あるとなるとどうなるでしょう?キューイングさせるときにいちいちキューの数をチェックして run() を走らせるってのはいつか忘れてしまいそうです。

また、個人的に再帰処理ってのはちょっと気持ち悪かったりします

ということで、イベントドリブンではなくデータドリブンにできる observe-js の出番です。


監視処理

先ほどの「キューされた処理の実行」までのコードの最後にある run() を削除し、以下の監視処理用のコードを先頭の方に追加します。

var observer = new ArrayObserver(items);

observer.open(function(splices) {
// console.log(splices);
if (splices[0].removed.length > 0 && items.length === 0) { // ①
console.log("queue is empty!");
}
if (splices[0].addedCount > 0 && splices[0].index === 0) { // ②
run();
}
if (splices[0].removed.length > 0 && items.length > 0) { // ③
run();
}
});

うまく動けば適当にQueueが増減して最後にコンソールログへメッセージが出るはずです。

どうでしょう?先ほどの「observe-jsを利用しない方法」よりわかりやすくなったと思いませんか?

データの追加とデータの処理、処理のハンドリングがきれいに分かれてそれぞれの役割が明確になり見通しがよくなったと思います。

簡単に説明すると、①では全削除の時の動きを指定し、②と③で実際に処理するタイミングを指定しています。

実際に渡される値は console.log(splices) で確認してみてください。

今回は items.push()setTimeout で行ったので splices の中身は確実?に1つで検知されました。ですが、もし setTimeout を使わなかった場合は、任意の固まりで呼び出される感じです。ここらへんはJavascriptの実装系によるのかもしれません。


まとめ

さて、ここまでのコードは以下になります。このくらいのコードでもかなり分かりやすいですね。

複雑なソースになればなるほど、機能を分離させることはバグ発生を抑えることに有効だと思います。

var items = [];

var observer = new ArrayObserver(items);
observer.open(function(splices) {
// console.log(splices);
if (splices[0].removed.length > 0 && items.length === 0) {
console.log("queue is empty!");
}
if (splices[0].addedCount > 0 && splices[0].index === 0) {
run();
}
if (splices[0].removed.length > 0 && items.length > 0) {
run();
}
});

function add(i) {
setTimeout(function(i) {
items.push(i);
document.getElementById("queue").innerHTML = items.join(", ");
}, 5000 * Math.random(), i);
}

for (var i=0; i<20; i++) {
add(i);
}

function run() {
setTimeout(function() {
var item = items.shift();
document.getElementById("queue").innerHTML = items.join(", ");
}, 1000);
}

最初に紹介した Dev:BOARD では、 add() でカードを作り、 run() で実際にカラムに追加という処理に分けています。

まあ、ここらへんは「松屋のように先にお金を払っておく方式」がいいか、「吉野家みたいに後からお金を払う方式」がいいかは好き嫌いが別れると思いますのでお好みでどうぞ