JSDeferredで、面倒な非同期処理とサヨナラ

第4回JSDeferredを使いJSアプリのパフォーマンスを最適化する

前回まででJSDeferredの基本的な使いかたを説明しました。

今回は「非同期処理を簡単にする」から一歩進んで、既存アプリケーションの「非同期処理でパフォーマンス改善する」ことや、その他応用できる部分について説明します。

UIスレッド

JavaScriptが担当するアプリケーションユーザインターフェイスの「はやさ」とはなんでしょうか? 単純に、最速で処理が終わることというのも一つですが、それを含めて最も重要なのは、ユーザにストレスを与えないことでしょう。多少実行速度が遅くても大抵待っていられますが、インターフェイスがなんの反応もしないと大きなストレスになります。

JavaScriptを実行するブラウザには、UIスレッドを呼ばれる、システム入出力を処理する流れがあります。JavaScriptは、その性質上どうしてもUIスレッド上で動かす必要があります(JavaScriptを実行中にページの内容が割り込みで変化されたりすると整合性がとれなくて困りますよね⁠⁠。

JavaScriptは、UIスレッド上で動くスクリプトであるため、JavaScript実行中、ブラウザは一切のUIに関する処理を行えなくなります。どんなブラウザであってもjavascript:for(;;);で数秒固まってしまうのはこのためです。ブラウザごとにUIスレッドの粒度は違いますが(ブラウザ全体で1つのUIスレッドであるか、ページごとであるか⁠⁠、少なくともページごとには存在します。

UIスレッドの実行をできるだけ止めない

UIスレッドを長時間(数百ミリ秒)止めると、ユーザに大きなストレスを与えます。あるいはあまりに長時間(数秒)になるとブラウザによってダイアログが表示されたりして、強制的に遮断されることになります。

リスト1 
var n = 1000000;

for (var i = 0; i < n; i++) {
  // do something
}

では、そうしないためにはどうすればよいかというと、単純にsetTimeout()関数を使って、一旦JavaScriptの実行を止めてしまいます。それにより、UIスレッドはJavaScript実行以外の処理を間に挟むことができます。

setTimeout()関数は指定時間後に指定した関数を実行する関数ですが、時間に0を指定すれば、UIスレッドが空き次第、JavaScriptを実行するような挙動にすることができます(実際にはブラウザはsetTimeoutの監視をそう頻繁には行わないため、どのブラウザであっても最低10msec程度は遅延します⁠⁠。

リスト2 
setTimeout(function () {
  // do something
}, 0);

例えば、forループをsetTimeout()関数を使い、できるだけブロックしないようにリスト1を記述しなおすと、以下のようになります。

リスト3
var n = 1000000;

var i = 0;
setTimeout(function () {
  if (i < n) {
    // do something
    i++;
    setTimeout(arguments.callee, 0);
  }
}, 0);

単純にforのループの切れ目でsetTimeoutするようにしただけですが、大変複雑になったように見えます。また、このループの処理の後にさらに処理を続けていきたいとき、面倒くさいですね。

全体が非同期の流れになっているので、ここでもJSDeferredが活躍できます。

JSDeferredを使うようにする

では、JSDeferredで処理を分割してUIスレッドの処理をブロックしないようにしてみましょう。まずはDeferred.next()関数がsetTimeout()相当のことをする(内部でsetTimeoutを使っている)ことを思いだして、上記setTimeoutの例(リスト3)を記述しなおしてみます。

リスト4 
var n = 1000000;

var i = 0;
next(function () {
  if (i < n) {
    // do something
    return next(arguments.callee);
  }
}).
next(function () {
  alert('done');
});

とりあえずこれで、ループが終ったあとの処理は簡単に追加できるようになりました。しかしループ自体はあまり綺麗ではありません。

とはいっても、JSDeferredにはloop()関数があり、これを使えば簡単にループを分割して処理できるようになります。

リスト5 
var n = 1000000;

loop(n, function (i) {
  // do something
}).
next(function () {
  alert('done');
});

とても簡単になりました! UIもブロックせず簡潔です。ただし、総合的な実行時間は、UIスレッドの実行を挟んだり、setTimeoutの処理をするため低下します。

さらに効率よくするために

今までの例では、setTimeoutであれloop()であれ、毎回の処理でUIスレッドに処理を返すため、各ループの処理が軽いがループ回数がやたらと多い場合には総合的な実行時間があまりにも遅くなりすぎます。

そこで、自動的に数十ミリ秒で処理を分割する関数を記述しておくと便利です。

リスト6 
function aloop (n, f) {
    var i = 0, end = {}, ret = null;
    return Deferred.next(function () {
        var t = (new Date()).getTime();
        divide: {
            do {
                if (i >= n) break divide;
                ret = f(i++);
            } while ((new Date()).getTime() - t < 20);
            return Deferred.call(arguments.callee);
        }
    });
}

このような関数を利用することで、ループを実行しつつ、20msecを超えた時点で自動的にUIスレッドに処理がかえるようになり、効率が多少良くなります。この関数はDeferredオブジェクトを返しますので、続きの処理はもちろんnext()で繋げられます。

パフォーマンス改善にJSDeferredを使う理由

これだけ簡単にループを分割し、UIスレッドのブロック具合を制御できるのであれば「とりあえずいくつかパフォーマンス改善を試してみる」という段階からこの方法をとることができます。

もしここでsetTimeout()によるプリミティブな方法をしていると、なかなかこの手のパフォーマンス改善はしにくく、優先順位が低くなってしまいますから、JSDeferredを最初から使っておく価値は十分あると思います。

その他の応用

これまでXMLHttpRequestやsetTimeoutの例ばかりでしたが、JSDeferredは非同期処理ならなんでも扱えます。例えばイベントもその一つです。

クリックイベントをDeferred化してステップ実行を実装してみます。

リスト7 
function onclick (element) {
  var d = new Deferred();
  element.addEventListener("click", function (e) {
    element.removeEventListener("click", arguments.callee, false);
    d.call(e);
  }, false);
  return de;
}

loop(10, function (n) {
  alert(n);
  return onclick(document.body);
});

たったこれだけでクリックするごとにワンステップループをすすめるコードを記述することができます。簡単ですね。おこなっていることは前回までの例と同じで、Deferredオブジェクトを生成していてcall()を呼んでいるだけです。ただし、Deferredオブジェクトを一回で使い捨ててしまうことが前提のため、removeEventListenerも呼んでいます。

まとめ

4回にわたり、JSDeferredの紹介をさせていただきました。

JSDeferredによって、JavaScriptにおける以下の点を解決することができることを説明しました。

  • 非同期処理を簡潔になる
  • パフォーマンス改善がしやすくなる

JSDeferredを知らない人がコードを読むと多少意味がわかりにくくなってしまう欠点もありますが、利便性と可読性のバランスを考えると、ぎりぎりのバランスではないかと思います。JSDeferredは簡潔になるように記述したつもりなので、完全にブラックボックスのライブラリとして使うのではなく、一度読んで中身を読んでから使うのがお奨めです。

最初のバージョンが出てから既に2年以上経過していますし、自分も今便利に使っているライブラリでもあります。これを機にJSDeferredないし、このような非同期処理の扱いかたについて興味を持っていただければ幸いです。

おすすめ記事

記事・ニュース一覧