引言
在那篇經典的關於jQuery1.5中Deferred使用方法介紹的文章中(譯文見這裡),有下面一段描述:
$.ajax() returns an object packed with other deferred-related methods. I discussed promise(), but you'll also find then(), success(), error(), and a host of others. You don't have access to the complete deferred object, though; only the promise, callback-binding methods, and the isRejected() and isResolved() methods, which can be used to check the state of the deferred.
But why not return the whole object? If this were the case, it would be possible to muck with the works, maybe pragmatically "resolve" the deferred, causing all bound callbacks to fire before the AJAX request had a chance to complete. Therefore, to avoid potentially breaking the whole paradigm, only return the dfd.promise().
這段話非常令人費解,我也是看了幾遍才看明白。大致的意思是:
$.ajax()返回一個對象(jqXHR,這是對原生的XMLHttpRequest的封裝),這個對象包含了deferred相關的函數,比如promise(), then(), success(), error(), isRejected(), isResolved()。
但是你發現沒,這裡面沒有resolve(), resolveWith(), reject(), rejectWith() 幾個函數,而這幾個函數才是用來改變deferred對象流程。也就是說$.ajax()返回了一個
只讀的deferred對象。
下面erichynds改用反問的語氣提出,為什麼不返回完整的deferred對象,而只返回只讀的deferred對象?
如果返回完整的deferred對象,那麼外部程序就能隨意的觸發deferred對象的回調函數,很有可能在AJAX請求結束前就觸發了回調函數(resolve),這就是與AJAX本身的邏輯相違背了。
所以為了避免不經意間改變任務的內部流程,我們應該只返回deferred的只讀版本(dfd.promise())。
為了說明$.ajax()和$.Deferred()返回的deferred對象的不同,請看下面的例子:
代碼如下:
// deferred對象所有的方法數組
var methods = 'done,resolveWith,resolve,isResolved,then,fail,rejectWith,reject,isRejected,promise'.split(','),
method,
ajaxMethods = [],
onlyInDeferredMethods = [];
for (method in $.ajax()) {
if ($.inArray(method, methods) !== -1) {
ajaxMethods.push(method);
}
}
for (method in $.Deferred()) {
if ($.inArray(method, methods) !== -1 && $.inArray(method, ajaxMethods) === -1) {
onlyInDeferredMethods.push(method);
}
}
// 存在於$.Deferred(),但是不存在於$.ajax()的deferred相關方法列表為:
// ["resolveWith", "resolve", "rejectWith", "reject"]
console.log(onlyInDeferredMethods);
反面教材
如果$.ajax()返回的對象包含resolve(), resolveWith(),可能會產生哪些影響呢?
我們還是用例子也說明,首先看看erichynds原文的第一個例子:
代碼如下:
// $.get, 異步的AJAX請求
var req = $.get('./sample.txt').success(function (response) {
console.log('AJAX success');
}).error(function () {
console.log('AJAX error');
});
// 添加另外一個AJAX回調函數,此時AJAX或許已經結束,或許還沒有結束
// 由於$.ajax內置了deferred的支持,所以我們可以這樣寫
req.success(function (response) {
console.log('AJAX success2');
});
console.log('END');
執行結果為:
END -> AJAX success -> AJAX success2
下面修改jQuery1.5源代碼,為$.ajax()的返回值添加resolve()和resolveWith()函數:
代碼如下:
// Attach deferreds
deferred.promise( jqXHR );
jqXHR.success = jqXHR.done;
jqXHR.error = jqXHR.fail;
jqXHR.complete = completeDeferred.done;
// 下面兩行是我們手工增加的,jQuery源代碼中沒有
jqXHR.resolve = deferred.resolve;
jqXHR.resolveWith = deferred.resolveWith;
然後,執行下面代碼:
代碼如下:
// $.get, 異步的AJAX請求
var req = $.get('./sample.txt').success(function (response) {
console.log('AJAX success');
}).error(function () {
console.log('AJAX error');
});
req.resolve();
// 添加另外一個AJAX回調函數,此時AJAX或許已經結束,或許還沒有結束
// 由於$.ajax內置了deferred的支持,所以我們可以這樣寫
req.success(function (response) {
console.log('AJAX success2');
});
console.log('END');
此時的執行結果為:
AJAX success -> AJAX success2 -> END
也就是說,在真實的AJAX請求結束之前,success的回調函數就已經被觸發了,出現錯誤。
為了更清楚的看清這一切,我們手工給success回調函數傳遞一些偽造的參數:
代碼如下:
// $.get, 異步的AJAX請求
var req = $.get('./sample.txt').success(function (response) {
console.log('AJAX success(' + response + ')');
});
req.resolve('Fake data');
// 添加另外一個AJAX回調函數,此時AJAX或許已經結束,或許還沒有結束
// 由於$.ajax內置了deferred的支持,所以我們可以這樣寫
req.success(function (response) {
console.log('AJAX success2(' + response + ')');
});
console.log('END');
此時的執行結果為:
AJAX success(Fake data) -> AJAX success2(Fake data) -> END
代碼分析
在深入jQuery代碼之前,先來看看jQuery.promise的文檔:
The deferred.promise() method allows an asynchronous function to prevent other code from interfering with the progress or status of its internal request. The Promise exposes only the Deferred methods needed to attach additional handlers or determine the state (then, done, fail, isResolved, and isRejected), but not ones that change the state (resolve, reject, resolveWith, and rejectWith).
If you are creating a Deferred, keep a reference to the Deferred so that it can be resolved or rejected at some point. Return only the Promise object via deferred.promise() so other code can register callbacks or inspect the current state.
大致意思是說,deferred.promise()用來阻止其它代碼修改異步任務的內部流程。Promise的對象只對外公開添加回調函數和檢測狀態的函數,而不包含修改狀態的函數。
如果你手工創建了一個deferred對象,那麼你要維持對這個deferred對象的引用,以此來修改狀態觸發回調函數。不過你的返回值應該是deferred.promise(),這樣外部程序可以添加回調函數或檢測狀態,而不能修改狀態。
至此,大家對promise應該有清晰的認識了。我們再來看下面兩段代碼,它們完成的功能完全一致:
代碼如下:
function getData() {
return $.get('/foo/');
}
function showDiv() {
// 正確代碼。推薦做法。
return $.Deferred(function (dfd) {
$('#foo').fadeIn(1000, dfd.resolve);
}).promise();
}
$.when(getData(), showDiv()).then(function (ajaxResult) {
console.log('The animation AND the AJAX request are both done!');
});
代碼如下:
function getData() {
return $.get('/foo/');
}
function showDiv() {
// 正確代碼。不推薦這麼做。
return $.Deferred(function (dfd) {
$('#foo').fadeIn(1000, dfd.resolve);
});
}
$.when(getData(), showDiv()).then(function (ajaxResult) {
console.log('The animation AND the AJAX request are both done!');
});
雖然上面兩段代碼完成相同的任務,並且似乎第二段代碼更加簡潔,但是第二段代碼卻不是推薦的做法。
因為任務(showDiv)本身狀態的更改應該保持在任務內部,而不需要對外公開,對外只需要公開一個promise的只讀deferred對象就行了。
最後,我們來看看Deferred相關源代碼:
代碼如下:
// Promise相關方法數組
promiseMethods = "then done fail isResolved isRejected promise".split( " " ),
jQuery.extend(
// 完備的deferred對象(具有兩個回調隊列)
Deferred: function (func) {
var deferred = jQuery._Deferred(),
failDeferred = jQuery._Deferred(),
promise;
// 添加then, promise 以及出錯相關的deferred方法
jQuery.extend(deferred, {
then: function (doneCallbacks, failCallbacks) {
deferred.done(doneCallbacks).fail(failCallbacks);
return this;
},
fail: failDeferred.done,
rejectWith: failDeferred.resolveWith,
reject: failDeferred.resolve,
isRejected: failDeferred.isResolved,
// 返回deferred對象的只讀副本
// 如果將obj作為參數傳遞進去,則promise相關方法將會添加到這個obj上
promise: function (obj) {
if (obj == null) {
if (promise) {
return promise;
}
promise = obj = {};
}
var i = promiseMethods.length;
while (i--) {
obj[promiseMethods[i]] = deferred[promiseMethods[i]];
}
return obj;
}
});
// 確保只有一個回調函數隊列可用,也就是說一個任務要麼成功,要麼失敗
deferred.done(failDeferred.cancel).fail(deferred.cancel);
// 刪除cancel函數
delete deferred.cancel;
// 將當前創建的作為參數傳遞到給定的函數中
if (func) {
func.call(deferred, deferred);
}
return deferred;
});
如果你覺得上面的代碼閱讀比較困難,沒關系我寫了一個簡單的類似代碼:
代碼如下:
Arr = function () {
var items = [],
promise,
arr = {
add: function (item) {
items.push(item);
},
length: function () {
return items.length;
},
clear: function () {
items = [];
},
promise: function () {
if (promise) {
return promise;
}
var obj = promise = {};
obj.add = arr.add;
obj.length = arr.length;
obj.promise = arr.promise;
return obj;
}
};
return arr;
}
上面代碼定義了一個Arr,用來生成一個數組對象,包含一些方法,比如add(), length(), clear(), promise()。
其中promise()返回當前Arr對象的一個副本,只能向其中添加元素,而不能清空內部數組。
代碼如下:
var arr = Arr();
arr.add(1);
arr.add(2);
// 2
console.log(arr.length());
arr.clear();
// 0
console.log(arr.length());
var arr = Arr();
arr.add(1);
arr.add(2);
// 2
console.log(arr.length());
var promise = arr.promise();
promise.add(3);
promise.add(4);
// 4
console.log(promise.length());
// Error: TypeError: promise.clear is not a function
promise.clear();
deferred.promise()與deferred.promise().promise()
還記得前面提到的那兩個完成相同功能的代碼麼?
代碼如下:
function getData() {
return $.get('/foo/');
}
function showDiv() {
// 這裡返回promise()或者直接返回deferred對象,代碼都能正確運行。
return $.Deferred(function (dfd) {
$('#foo').fadeIn(1000, dfd.resolve);
}).promise();
}
$.when(getData(), showDiv()).then(function (ajaxResult) {
console.log('The animation AND the AJAX request are both done!');
});
那麼你有沒有思考過,為什麼這兩種方式都能運行呢?
如果你深入jQuery的源代碼,你會發現$.when(obj1, obj2, ...)在內部實現時會獲取obj1.promise():
代碼如下:
if ( object && jQuery.isFunction( object.promise ) ) {
object.promise().then( iCallback(lastIndex), deferred.reject );
}
所以我們來看上面showDiv的返回結果:
如果是deferred對象的話,$.when()通過下面方式得到promise:
$.Deferred().promise()
如果是deferred.promise()對象的話,$.when()通過下面方式得到promise:
$.Deferred().promise().promise()
那麼是不是說:$.Deferred().promise() === $.Deferred().promise().promise()
我們還是通過示例來驗證我們的想法:
代碼如下:
var deferred = $.Deferred(),
promise = deferred.promise();
// true
promise === promise.promise();
// true
promise === promise.promise().promise().promise();
當然,這個結果是推理出來的,如果我們直接看Deferred的源代碼,也很容易看出這樣的結果:
代碼如下:
promise: function (obj) {
if (obj == null) {
// 在這裡,如果promise已經存在(已經調用過.promise()),就不會重新創建了
if (promise) {
return promise;
}
promise = obj = {};
}
var i = promiseMethods.length;
while (i--) {
obj[promiseMethods[i]] = deferred[promiseMethods[i]];
}
return obj;
}
總結
1. deferred.promise()返回的是deferred對象的只讀屬性。
2. 建議任務不要返回deferred對象,而是返回deferred.promise()對象。這樣外部就不能隨意更改任務的內部流程。
3. deferred.promise() === deferred.promise().promise() (上面我們分別從代碼推理,和源代碼分析兩個角度得到這個結論)
本文由三生石上原創,博客園首發,轉載請注明出處。