こんにちは、Research Panel Asia の関口(@takkyuuplayer)です。
jQueryを用いた非同期処理の待ち合わせについて書きたいと思います。 昨今WebにリッチなUIが求められていますが、その際によく遭遇するのが次のような処理です。
「処理A(非同期)が終わってからその結果を元に処理Bを行いたい。」
jQuery.getなんかでデータを取ってくる際には必須ですね。このような非同期処理の待ち合わせはjQuery.Deferredを使うと簡単に実現可能です。
jQuery.Deferred
ex1.html<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.4/underscore-min.js"></script>
</head>
<body>
<ul id="async_test">
<li>0</li>
</ul>
<script language="javascript">
var time_ms = 1000;
var ajax = function(arg) {
var deferred = $.Deferred();
setTimeout(function() {
deferred.resolve(arg+1);
}, time_ms);
return deferred.promise();
}
var tmpl = _.template('<li><%= count %> : <%= wait %>ms later</li>');
ajax(0).done(function(ret) {
$('ul#async_test').append(tmpl({count:ret, wait:time_ms}));
});
</script>
</body>
</html>
また、jQuery.whenを使って「処理Aと処理A’が両方終わってから、処理Bを行う」事もできます。
jQuery.when
ex2.html : ※下記にはex1.htmlとの変更部分のみ示してあります。var tmpl = _.template('<li><%= count1 %> / <%= count2 %> </li>');
$.when(ajax(0), ajax(100)).done(function(ret1, ret2) {
$('ul#async_test').append(tmpl({count1:ret1, count2:ret2}));
});
便利ですね。任意個数の待ち合わせを行うにはどうすればよいでしょうか?引数を配列にすることで、「処理A, A’, A’’, ....が終わってから処理Bを行う」事ができます。
複数処理の同時待ち合わせ
ex3.html : ※下記にはex1.htmlとの変更部分のみ示してあります。var deferreds = [];
for (var i=0; i<20; i++) {
deferreds.push(ajax(i*10));
}
var tmpl = _.template('<li><%= text %></li>');
$.when.apply(null, deferreds).done(function() {
var rets = Array.prototype.slice.call(arguments);
$('ul#async_test').append(tmpl({text:rets.join(' / ')}));
});
ただし、実際にex3.htmlのようなコードを書くことは稀でしょう。非同期処理はjQuery.getやjQuery.postといった、HTTPリクエストであることが多いと思います。ex3.htmlでは20ものHTTPリクエストを同時に発生させてしまいます。そこで求められるのは「処理Aが終わったら処理A’を行い、それが終わったら処理A’’を行い・・・最後に処理Bを行う」事です。私は悩んだ末以下のように再帰で解決しました。
順番に待ち合わせる
ex4.html : ※下記にはex1.htmlとの変更部分のみ示してあります。var deferreds = [];
var max = 20;
for (var i=0; i<max; i++) {
deferreds.push(function(arg) { return ajax(arg*10); });
}
var rets = [];
var do_next = function() {
if(deferreds.length == 0 ) {
var tmpl = _.template('<li>Finished</li>');
$('ul#async_test').append(tmpl({text:rets.join(' / ')}));
return;
}
var arg = max - deferreds.length;
var deferred = deferreds.shift();
deferred(arg).done(function(ret) {
rets.push(ret);
var tmpl = _.template('<li>Process : <%= text %></li>');
$('ul#async_test').append(tmpl({text:rets.join(' / ')}));
do_next();
});
};
do_next();
ajaxに渡す引数の処理がいけていませんが、deferredsの中に入れるのをnewしたオブジェクトにしておけば解決できるでしょう。