queue()を使おう

この記事はToshiyuki Hayashiさんの企画、d3.js Advent Calendar 2013への参加記事です。
といっても今回取り上げる queue()自身は直接D3に関連しているわけではありません。しかしこのライブラリは、

  • D3の作者マイク・ボストック氏作成のライブラリである。
  • そのため氏の作成する D3サンプルによく用いられている(※)。
  • したがって、少なくともD3では安心して利用することができる(D3プログラミングで充分にテストされている)。

の3点から、D3プログラマが覚えておいて損はないライブラリだと思います。

queue()とは

queue()は Github からダウンロードできます。

Github – mbostock / queue

同ページの README にある通り 『Queue.js とは、JavaScript のもう一つの(yet another)非同期ヘルパーライブラリである。並行処理をスムーズに行うための Async.js の最小バージョンと理解してもらえば良いだろう。あるいは TameJS からコードジェネレータを省いたものと理解してもらっても良い。』 (拙訳)です。

D3 で data visualization を行うとき、複数の CSV や TSV、JSON ファイルを読み込むことがよくあります。外部ファイルの読み込みは当然非同期処理になるのですが、読み込みファイルが多くなるにつれコールバック関数のネストがどんどん深くなり、そこにエラー処理や終了処理、あるいは読み込み負荷軽減のための遅延処理(タイマー処理)が加わるとコードの可読性がどんどん悪くなります(いわゆるコールバック地獄)。

非同期ヘルパーライブラリとはこうしたコードをシンプルに書くためのライブラリです。その目的は、一にも二にも、コードの可読性を高める(つまりデバグ・メンテを容易にする)ことです。内部的には深いネスト処理やタイマー処理と同等のことをしているのですが、非同期ヘルパーライブラリを使うことで、出来上がったコードが、プログラマの自然な思考に沿ったコードとなるのです。もちろん、実際には内部で非同期処理が行われていることはしっかり意識しておく必要があります(さもないと思わぬ罠に陥ります)。

jQueryプログラマであれば、jQuerry.Deferred をシンプルにしたもの、と理解すれば良いでしょう。

jQuery.Deferred はちょっととっつきが悪い(覚えるのに若干骨が折れる)ライブラリですが、queue()は最小限の機能にしぼってあるだけあって、シンプルですぐに覚えられます。シンプルかつ最小限ですが、D3 プログラマの遭遇するほとんどの必要条件を満たしているはずです。

queue()の実例

具体的に例を見てみましょう(※)。次のコードはマイク氏のサンプル「Choropleth(コロプレス)」の一部です。元ページには実際の画面とソースコード全体が掲載されていますので、ぜひ別窓で開きながらご確認ください。

...
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/queue.v1.min.js"></script> // ※1
...
queue() // ※2
	 .defer(d3.json, "/mbostock/raw/4090846/us.json") // ※3
	 .defer(d3.tsv, "unemployment.tsv", function(d) { rateById.set(d.id, +d.rate); })
	 .await(ready); // ※4
function ready(error, us) {
     ...
}

このコードの意味は「jsonファイルとtsv(タブ区切り)ファイルを一つずつ読み込み、両方の読み込みが完了した時点で関数readyを呼ぶ」です。

まず※1の script タグで queueライブラリを読み込み、次にコード内で、queue()によりキューオブジェクトを生成します(※2)。

そして登録したい非同期処理ごとに defer を使ってタスクを登録して行きます(※3)。

defer は英語で「先延ばしする」という意味ですが、ここでは queue() で作成した queue オブジェクトのリストに非同期処理を追加することを意味しています。つまり実行キューにいったん登録し、条件が整うまで実行を先延ばしする、という意味です。大きなファイルを同時に多数読み込みこんで PC やネットワークに負荷をかけるのを避けるのが目的です。

もちろんファイルサイズ・ファイル数が小さいときは即時実行しても問題はありません。遅延処理が必要ない場合でも、queue() を使う価値(可読性の向上)は充分あります。

最初の defer は、json ファイルを読み込むための d3 のメソッド d3.json をキューに登録し(読み込むファイルは “/mbstock/raw/4090846/us.json”)、二番目の defer は、tsv ファイルを読み込むための d3 のメソッド d3.tsv をキューに登録しています(読み込むファイルは “unemplyment.tsv”)。後者では三つ目の引数として無名関数をコールバック関数に指定してます。

ここではqueue()に引数(parallerism=同時実行数)を指定していないため、登録したタスクはいずれも即座に実行されます。

最後にすべてのキューが処理されたあと(各キューにコールバック関数が指定されている場合は、そのコールバック関数がすべて完了した後)、await() の引数に指定したコールバック関数(ここでは ready )が実行されます(※4)。もちろん関数名は任意ですので、ready でも finished でも omotenashi でも好きな名前をつけてかまいません。

何層にもネストしたコードを書くのに比べ、自然な思考の流れに沿った分かりやすいコードになることが理解いただけたと思います。

queue() API リファレンス

Github – mbostock / queueAPI Reference の拙訳です。

queue([parallelism])

指定した parallelism(同時実行数) の新しい queue を生成する。parallelism が指定されなかった場合、同時実行数は無限となる。指定する場合は正の整数でなければならない。たとえば parallelism が 1 の場合、すべてのタスクは一つずつ順に実行される。parallelism が 3 の場合、最大 3 つのタスクまで並行して実行される。この関数はブラウザにリソースを読み込む場合などに用いると便利である。

queue.defer(task[, arguments ...])

指定した非同期の task 関数をキューに追加する。その際、任意の個数の引数を指定することができる。task は通常、追加の引数(ファイル名など)やコールバック関数とともに呼ばれる。その際コールバック関数は、task の完了した時点で実行される。defer で task を登録するのは await でコールバックを指定する前で無ければならない。await の後に defer を指定した場合の動作は定義されない。

queue.await(callback)
queue.awaitAll(callback)

defer で登録したすべての task が完了した時点で実行されるコールバック関数を設定する。エラーが発生した場合、最初に発生したエラーがコールバック関数の最初の引数に渡される。エラーが発生しなかった場合は最初の引数には null が渡される。queue.await の場合、各 task の結果は、それぞれ別の引数としてコールバック関数の二つ目以降の引数に渡される。queue.awaitAll の場合、task のすべての結果は、一つの配列としてコールバック関数の二つ目の引数に渡される。await または awaitAllを実行する前にすべての task のコールバック関数が完了していた場合、await または awaitAll コールバック関数は即座に実行される。このメソッドは各 task を登録した後に一度しか実行することができない。複数回実行された場合、もしくは task の登録前に実行された場合の queue の動作は定義されない。

callback について

上記 callback は Node.js の記法に従っている。すなわち、最初の引数(任意)にはエラーオブジェクトが返され、二つ目の引数にはタスクの結果が返される。Queue.js は、複数の結果を返す非同期関数はサポートしていない。しかし複数の結果を一つのオブジェクトもしくは配列に変換するラッパー関数を作成することで、同様の機能が得られるだろう。

queue() のサンプル

マイク氏自身による queue() のサンプル

私も何かサンプル作りたかったのですが、時間が足りなかったのでまた後日… m(。_。)m



コメントを残す


− 一 = 6