Edited at

Deferredな $.ajax をラップしてコールバック関数(done/fail)に共通処理を追加する

More than 3 years have passed since last update.


Deferred でない場合

コールバック処理を従来の success, error, complete で指定する方法。

こんな感じでプロジェクト特有の共通処理などを追加していることが多いと思います。


実装


var myAjax = function(arg) {
var opt = $.extend({}, $.ajaxSettings, arg);

opt.success = (function(func) {
return function(data, statusText, jqXHR) {
console.log('success時の共通処理 ...');

if (func) {
func(data, statusText, jqXHR);
}
};
})(opt.success);

opt.error = (function(func) {
return function(jqXHR, statusText, errorThrown) {
console.log('error時の共通処理 ...');

if (func) {
func(jqXHR, statusText, errorThrown);
}
};
})(opt.error);

opt.complete = (function(func) {
return function(jqXHR, statusText) {
console.log('complete時の共通処理 ...');

if (func) {
func(jqXHR, statusText);
}
};
})(opt.complete);

return $.ajax(opt);
};


使い方

myAjax({

url: '/hoge',
type: 'GET',
dataType: 'json',
data: {
param1: 'value1',
param2: 'value2'
},
success: function(json, statusText, jqXHR) {
console.log('SUCCESS!');
},
error: function(jqXHR, statusText, errorThrown) {
console.log('ERROR!');
},
complete: function(jqXHR, statusText) {
console.log('COMPLETE!');
}
});


実行結果

success時の共通処理 ...

SUCCESS!
complete時の共通処理 ...
COMPLETE!


Deferred な場合

コールバック処理をPromiseの done, fail, always で指定する方法。


実装


  • ポイント


    • jQuery1.8以降、$.ajax の結果(jqXHR)はXMLHttpRequestのプロパティ・メソッドに加えて、Promiseのプロパティ・メソッドも実装している

    • return 時、(myAjax内で生成した) DeferredオブジェクトをPromise化( defer.promise() )し、外部からは then()、done()、fail() といったメソッド以外、Deferredオブジェクトを操作出来ないようにする

    • return 時、(myAjax内で生成した) Deferredオブジェクトに $.ajax の結果(jqXHR) をマージすることで、元々のjqXHRのプロパティ・メソッドを使用できるのはそのままに、外部から処理(done、fail)を注入できるようにするのがミソ



var myAjax = function(arg) {

var opt = $.extend({}, $.ajaxSettings, arg);
var jqXHR = $.ajax(opt);

var defer = $.Deferred();

jqXHR.done(function(data, statusText, jqXHR) {
console.log('done(=success)時の共通処理 ...');

// defer.resovle ではなくて defer.resolveWith で
// myAjax(...).done() 内でのthisのコンテキストを
// 明示的に指定する
defer.resolveWith(this, arguments);
});

jqXHR.fail(function(jqXHR, statusText, errorThrown) {
console.log('fail(=error)時の共通処理 ...');

// defer.reject ではなくて defer.rejectWith で
// myAjax(...).fail() 内でのthisのコンテキストを
// 明示的に指定する
defer.rejectWith(this, arguments);
});

jqXHR.always(function() {
console.log('always(=complete)時の共通処理 ...');
});

return $.extend({}, jqXHR, defer.promise());
};


使い方

myAjax({

url: '/hoge',
type: 'GET',
dataType: 'json',
data: {
param1: 'value1',
param2: 'value2'
}
}).done(function(json, statusText, jqXHR) {
console.log('DONE!');
}).fail(function(jqXHR, statusText, errorThrown) {
console.log('FAIL!');
}).always(function() {
console.log('ALWAYS!');
});


実行結果

done(=success)時の共通処理 ...

DONE!
always(=complete)時の共通処理 ...
ALWAYS!


ハマったポイント


① ラッパー内でDeferredオブジェクトを生成しない場合

つまり、

var myAjax = function(arg) {

var opt = $.extend({}, $.ajaxSettings, arg);
var jqXHR = $.ajax(opt);

jqXHR.done(function(data, statusText, jqXHR) {
console.log('done(=success)時の共通処理 ...');
});

jqXHR.always(function() {
console.log('always(=complete)時の共通処理 ...');
});

return jqXHR;
};

myAjax({
// ...
}).done(function(json, statusText, jqXHR) {
console.log('DONE!');
}).always(function() {
console.log('ALWAYS!');
});

だとどうなるか?

こうなる。

done(=success)時の共通処理 ...

DONE!
always(=complete)時の共通処理 ...
ALWAYS!

え?いいんじゃないの?

でも、こんな時ダメ

var myAjax = function(arg) {

var opt = $.extend({}, $.ajaxSettings, arg);
var jqXHR = $.ajax(opt);

jqXHR.done(function(data, statusText, jqXHR) {
if (data.isXXX) {
console.log('何かをハンドリングして、この場合はここでdoneを終わりたい!');
return;
}
});

jqXHR.always(function() {
console.log('always(=complete)時の共通処理 ...');
});

return jqXHR;
};

結果。

何かをハンドリングして、この場合はここでdoneを終わりたい!

DONE!  ←★この処理は実行したくない★
always(=complete)時の共通処理 ...
ALWAYS!

done/failの共通処理内でdone/failを切り上げられない。

これはこう書いたのと同じ

$.ajax({

// ...
}).done(function() {
if (data.isXXX) {
console.log('何かをハンドリングして、この場合はここでdoneを終わりたい!');
return;
}
}).done(function() {
console.log('DONE!');
})...

2つのdoneが別々のコールバック処理としてイベントキューに入れられて順番(=FIFO)に実行されるため。


② ラッパーがreturnするDeferred(Promise)オブジェクトのmerge順が逆

もし、

var myAjax = function(arg) {

var opt = $.extend({}, $.ajaxSettings, arg);
var jqXHR = $.ajax(opt);

var defer = $.Deferred();

// ...

// return $.extend({}, jqXHR, defer.promise());
return $.extend({}, defer.promise(), jqXHR);
};

とすると、deferのdoneやfailがjqXHRのそれらで上書きされてしまうため、①と同じ結果になる。


③ ラッパーのdone/failのコールバック関数内で参照するthisのコンテキスト

下記のようにdefer.resolveでコールバック関数(done)を実行できるようにして何の問題もなさそうだけど...

var myAjax = function(arg) {

var opt = $.extend({}, $.ajaxSettings, arg);
var jqXHR = $.ajax(opt);

var defer = $.Deferred();

jqXHR.done(function(data, statusText, jqXHR) {
console.log('done(=success)時の共通処理 ...');

// ★ ↓↓↓ ★
defer.resolve(data, statusText, jqXHR);
// ★ ↑↑↑ ★
});

// ...

return $.extend({}, jqXHR, defer.promise());
};

myAjax({
// ...
}).done(function() {
// ↓ このthisは何者??
console.log(this);
})...

myAjax実行時のdoneのコールバック内のthisを見てみると

myAjax内で生成したDeferred(Promise)オブジェクト相当になっている。

Pentan 2.png

そこで下記のようにdefer.resolveWithでコールバック関数内でのthisのコンテキストを明示的に指定してあげると...

var myAjax = function(arg) {

var opt = $.extend({}, $.ajaxSettings, arg);
var jqXHR = $.ajax(opt);

var defer = $.Deferred();

jqXHR.done(function(data, statusText, jqXHR) {
console.log('done(=success)時の共通処理 ...');

// ★ ↓↓↓ ★
defer.resolveWith(this, arguments);
// ★ ↑↑↑ ★
});

// ...

return $.extend({}, jqXHR, defer.promise());
};

myAjax({
// ...
}).done(function() {
// ↓ このthisは何者??
console.log(this);
})...

バッチリ元々の$.ajaxのthis相当に👍

Pentan.png