複数のAjaxタスクをシーケンシャルに実行するには?

#javascript, #jquery

処理1 > 処理2 > 処理3のように複数のAjaxを直列実行しなければいけなくなったとき、以下のようにコールバックがネストしていってガチガチのコーディングになってしまう。このネスト構造、API1とAPI3の順番を入れ替える修正とかイヤになりますね。

今回はこういう処理をキレイに可読性高く記述しようという話。

1
2
3
4
5
6
7
$.getJSON('path to API1', function() {
$.getJSON('path to API2', function() {
$.getJSON('path to API3', function() {
// つづく ...
});
});
});

jQuery.Deferredを使おう

これらを解決するためにjQuery.Deferredを使用します。
jQueryのAjaxはPromiseオブジェクトを返してくれるので下のようにタスクを定義します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var task1 = function() {
return $.getJSON('path to API1').pipe(
function(res) { return res; },
function() { return 'task1 fail'; }
);
};
var task2 = function() {
return $.getJSON('path to API2').pipe(
function(res) { return res; },
function() { return 'task2 fail'; }
);
};
var task3 = function() {
return $.getJSON('path to API3').pipe(
function(res) { return res; },
function() { return 'task3 fail'; }
);
};
task1()
.pipe(task2)
.pipe(task3)
.fail(function() {
console.log('common fail', arguments);
})
.always(function() {
console.log('always', arguments);
});

pipe()でつなげればシーケンスにタスクを実行できます。
resolveなんだけどレスポンスを見てエラーとしたい場合はdoneのコールバックの中で

1
return $.Deferred().reject('error!').promise();

として、新しくrejectしたPromiseを返すと良いです。

これで簡潔にAjaxの直列処理を記述することができました。
jQuery.Deferred()スゴいネ!

もっと簡潔に書きたい

しかし、まだ違和感がありまして。。
この書き方だとtask1とtask2、task3でレベル感が違いますね。もっとこう、それぞれのタスクを同じレベル感で書きたいわけです。

pipeにはPromiseオブジェクトを渡せばいいので、大外でPromise返すとタスクのレベルが同じになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var dfd = $.Deferred(),
prms = dfd.promise();
prms
.pipe(task1)
.pipe(task2)
.pipe(task3)
.fail(function() {
console.log('common fail', arguments);
})
.always(function() {
console.log('always', arguments);
});
dfd.resolve();

これでtask1、2、3が直列処理されているんだな感が出てきました。

さらに簡潔に書きたい

でも、まだ違和感がありまして。。。

  • いちいちDeferredオブジェクト生成したくない
  • resolveを間違って消されるかも
  • ユーザータイプでtask2をすっ飛ばすとか
  • ステータスによってtask2とtask3の間にconfirmいれるとか
  • そもそもpipeとか書きたくないとか

では、タスクを渡したら勝手に直列処理してくれる関数あったら簡潔に分かりやすく書けるのではないか。ということで以下。

1
2
3
4
5
6
7
8
9
10
11
12
13
// jQuery のメソッドとして mixin
$.extend({
sequence: function() {
var tasks = Array.prototype.slice.call(arguments),
d = $.Deferred(),
p = d.promise();
$.each(tasks, function(i, task) {
p = p.pipe(task);
});
d.resolve();
return p;
}
});

使い方は以下。

1
2
3
4
5
6
7
$.sequence(task1, task2, task3)
.fail(function(r) {
console.log('common fail', r);
})
.always(function(r) {
console.log('common always', r);
});

勝手に思っておりますが、いい感じです。
そこはかとない直列感です。

可変なタスク配列を渡したいときはapply使って

1
$.sequence.apply($, [task1, task2, task3]);

とすればよいです。

task1、2、3は直列処理だけど、task4は並列とかは

1
$.when( $.sequence(task1, task2, task3), task4() );

でいけます。

おわりに