[TOC]
## 概述
deferred對象代表了將要完成的某種操作,并提供了一些方法,幫助用戶使用。它是jQuery對Promises接口的實現。jQuery的所有Ajax操作函數,默認返回的就是一個deferred對象。
簡單說,Promises是異步操作的通用接口,扮演代理人(proxy)的角色,將異步操作包裝成具有同步操作特性的特殊對象。異步操作的典型例子就是Ajax操作、網頁動畫、web worker等等。
由于JavaScript單線程的特點,如果某個操作耗時很長,其他操作就必需排隊等待。為了避免整個程序失去響應,通常的解決方法是將那些排在后面的操作,寫成“回調函數”(callback)的形式。這樣做雖然可以解決問題,但是有一些顯著缺點:
* 回調函數往往寫成函數參數的形式,形成所謂的“持續傳遞風格”(即參數就是下一步操作,Continuation-passing style),導致函數的輸入和輸出非常混亂,整個程序的可閱讀性差;
* 回調函數往往只能指定一個,如果有多個操作,就需要改寫回調函數。
* 除了正常的報錯機制,錯誤還可能通過回調函數的形式返回,增加了除錯和調試的難度。
* 正常的函數輸入和輸出可以區分得很清楚,回調函數使得函數的輸出不再重要。
Promises就是為了解決這些問題而提出的,它的主要目的就是取代回調函數,成為非同步操作的解決方案。它的核心思想就是讓非同步操作返回一個對象,其他操作都針對這個對象來完成。比如,假定ajax操作返回一個Promise對象。
~~~
var promise = get('http://www.example.com');
~~~
然后,Promise對象有一個then方法,可以用來指定回調函數。一旦非同步操作完成,就調用指定的回調函數。
~~~
promise.then(function (content) {
console.log(content)
})
~~~
可以將上面兩段代碼合并起來,這樣程序的流程看得更清楚。
~~~
get('http://www.example.com').then(function (content) {
console.log(content)
})
~~~
在1.7版之前,jQuery的Ajax操作采用回調函數。
~~~
$.ajax({
url:"/echo/json/",
success: function(response)
{
console.info(response.name);
}
});
~~~
1.7版之后,Ajax操作直接返回Promise對象,這意味著可以用then方法指定回調函數。
~~~
$.ajax({
url: "/echo/json/",
}).then(function (response) {
console.info(response.name);
});
~~~
## deferred對象的方法
### 基本用法
(1)生成deferred對象
第一步是通過$.Deferred()方法,生成一個deferred對象。
~~~
var deferred = $.Deferred();
~~~
(2)deferred對象的狀態
deferred對象有三種狀態。
* pending:表示操作還沒有完成。
* resolved:表示操作成功。
* rejected:表示操作失敗。
state方法用來返回deferred對象當前狀態。
~~~
$.Deferred().state() // 'pending'
$.Deferred().resolve().state() // 'resolved'
$.Deferred().reject().state() // 'rejected'
~~~
(3)改變狀態的方法
resolve方法將deferred對象的狀態從pending改為resolved,reject方法則將狀態從pending改為rejected。
~~~
var deferred = $.Deferred();
deferred.resolve("hello world");
~~~
resolve方法的參數,用來傳遞給回調函數。
(4)綁定回調函數
deferred對象在狀態改變時,會觸發回調函數。
done方法指定狀態變為resolved(操作成功)時的回調函數;fail方法指定狀態變為rejected(操作失敗)時的回調函數;always方法指定,不管狀態變為resolved或rejected,都會觸發的方法。
~~~
var deferred = $.Deferred();
deferred.done(function(value) {
console.log(value);
}).resolve('hello world');
// hello world
~~~
上述三種方法都返回的原有的deferred對象,因此可以采用鏈式寫法,在后面再鏈接別的方法(包括done和fail在內)。
~~~
$.Deferred().done(f1).fail(f2).always(f3);
~~~
### notify() 和 progress()
progress()用來指定一個回調函數,當調用notify()方法時,該回調函數將執行。它的用意是提供一個接口,使得在非同步操作執行過程中,可以執行某些操作,比如定期返回進度條的進度。
~~~
var userProgress = $.Deferred();
var $profileFields = $("input");
var totalFields = $profileFields.length
userProgress.progress(function (filledFields) {
var pctComplete = (filledFields/totalFields)*100;
$("#progress").html(pctComplete.toFixed(0));
});
userProgress.done(function () {
$("#thanks").html("Thanks for completing your profile!").show();
});
$("input").on("change", function () {
var filledFields = $profileFields.filter("[value!='']").length;
userProgress.notify(filledFields);
if (filledFields == totalFields) {
userProgress.resolve();
}
});
~~~
### then方法
(1)概述
then方法的作用也是指定回調函數,它可以接受三個參數,也就是三個回調函數。第一個參數是resolve時調用的回調函數(相當于done方法),第二個參數是reject時調用的回調函數(相當于fail方法),第三個參數是progress()方法調用的回調函數。
~~~
deferred.then( doneFilter [, failFilter ] [, progressFilter ] )
~~~
(2)返回值
在jQuery 1.8之前,then()只是.done().fail()寫法的語法糖,兩種寫法是等價的。在jQuery 1.8之后,then()返回一個新的promise對象,而done()返回的是原有的deferred對象。如果then()指定的回調函數有返回值,該返回值會作為參數,傳入后面的回調函數。
~~~
var defer = jQuery.Deferred();
defer.done(function(a,b){
return a * b;
}).done(function( result ) {
console.log("result = " + result);
}).then(function( a, b ) {
return a * b;
}).done(function( result ) {
console.log("result = " + result);
}).then(function( a, b ) {
return a * b;
}).done(function( result ) {
console.log("result = " + result);
});
defer.resolve( 2, 3 );
~~~
在jQuery 1.8版本之前,上面代碼的結果是:
~~~
result = 2
result = 2
result = 2
~~~
在jQuery 1.8版本之后,返回結果是
~~~
result = 2
result = 6
result = NaN
~~~
這一點需要特別引起注意。
~~~
$.ajax( url1, { dataType: "json" } )
.then(function( data ) {
return $.ajax( url2, { data: { user: data.userId } } );
}).done(function( data ) {
// 從url2獲取的數據
});
~~~
上面代碼最后那個done方法,處理的是從url2獲取的數據,而不是從url1獲取的數據。
(3)對返回值的修改
利用then()會修改返回值這個特性,我們可以在調用其他回調函數之前,對前一步操作返回的值進行處理。
~~~
var post = $.post("/echo/json/")
.then(function(p){
return p.firstName;
});
post.done(function(r){ console.log(r); });
~~~
上面代碼先使用then()方法,從返回的數據中取出所需要的字段(firstName),所以后面的操作就可以只處理這個字段了。
有時,Ajax操作返回json字符串里面有一個error屬性,表示發生錯誤。這個時候,傳統的方法只能是通過done()來判斷是否發生錯誤。通過then()方法,可以讓deferred對象調用fail()方法。
~~~
var myDeferred = $.post('/echo/json/', {json:JSON.stringify({'error':true})})
.then(function (response) {
if (response.error) {
return $.Deferred().reject(response);
}
return response;
},function () {
return $.Deferred().reject({error:true});
}
);
myDeferred.done(function (response) {
$("#status").html("Success!");
}).fail(function (response) {
$("#status").html("An error occurred");
});
~~~
上面代碼中,不管是通信出錯,或者服務器返回一個錯誤,都會調用reject方法,返回一個新的deferred對象,狀態為rejected,因此就會觸發fail方法指定的回調函數。
關于error的處理,jQuery的deferred對象與其他實現Promises規范的函數庫有一個重大不同。就是說,如果deferred對象執行過程中,拋出一個非Promises對象的錯誤,那么將不會被后繼的then方法指定的rejected回調函數捕獲,而會一直傳播到應用程序層面。為了代碼行為與Promises規范保持一致,建議出錯時,總是使用reject方法返回錯誤。
~~~
d = $.Deferred()
d.then(function(){
throw new Error('err')
}).fail(function(){
console.log('fail')
})
d.resolve()
// Error: err
~~~
上面代碼中,then的回調函數拋出一個錯誤,按照Promises規范,應該被fail方法的回調函數捕獲,但是jQuery的部署是上升到應用程序的層面。
(4)回調函數的返回值
如果回調函數返回deferred對象,則then方法的返回值將是對應這個返回值的promise對象。
~~~
var d1 = $.Deferred();
var promise = $.when('Hello').then(function(h){
return $.when(h,d1);
})
promise.done(function (s1,s2) {
console.log(s1);
console.log(s2);
})
d1.resolve('World')
// Hello
// World
~~~
上面代碼中,done方法的回調函數,正常情況下只能接受一個參數。但是由于then方法的回調函數,返回一個when方法生成的deferred對象,導致它可以接受兩個參數。
### pipe方法
pipe方法接受一個函數作為參數,表示在調用then方法、done方法、fail方法、always方法指定的回調函數之前,先運行pipe方法指定的回調函數。它通常用來對服務器返回的數據做初步處理。
### 與Promise A+規格的差異
Promise事實上的標準是社區提出的Promise A+規格,jQuery的實現并不完全符合Promise A+,主要是對錯誤的處理。
~~~
var promise2 = promise1.then(function () {
throw new Error("boom!");
});
~~~
上面代碼在回調函數中拋出一個錯誤,Promise A+規定此時Promise實例的狀態變為reject,該錯誤被下一個catch方法指定的回調函數捕獲。但是,jQuery的Deferred對象此時不會改變狀態,亦不會觸發回調函數,該錯誤一般情況下會被window.onerror捕獲。換句話說,在Deferred對象中,總是必須使用reject方法來改變狀態。
## promise對象
(1)概念
一般情況下,從外部改變第三方完成的異步操作(比如Ajax)的狀態是毫無意義的。為了防止用戶這樣做,可以在deferred對象的基礎上,返回一個針對它的promise對象。
簡單說,promise對象就是不能改變狀態的deferred對象,也就是deferred的只讀版。或者更通俗地理解成,promise是一個對將要完成的任務的承諾,排除了其他人破壞這個承諾的可能性,只能等待承諾方給出結果。
你可以通過promise對象,為原始的deferred對象添加回調函數,查詢它的狀態,但是無法改變它的狀態,也就是說promise對象不允許你調用resolve和reject方法。
(2)生成promise對象
deferred對象的promise方法,用來生成對應的promise對象。
~~~
function getPromise(){
return $.Deferred().promise();
}
try{
getPromise().resolve("a");
} catch(err) {
console.log(err);
}
// TypeError
~~~
上面代碼對promise對象,調用resolve方法,結果報錯。
jQuery的ajax() 方法返回的就是一個promise對象。此外,Animation類操作也可以使用promise方法。
~~~
$('body').toggle('blinds').promise().then(
function(){
$('body').toggle('blinds')
}
)
~~~
## 輔助方法
deferred對象還有一系列輔助方法,使它更方便使用。
### $.when()方法
$.when()接受多個deferred對象作為參數,當它們全部運行成功后,才調用resolved狀態的回調函數,但只要其中有一個失敗,就調用rejected狀態的回調函數。它相當于將多個非同步操作,合并成一個。實質上,when方法為多個deferred對象,返回一個單一的promise對象。
~~~
$.when(
$.ajax( "/main.php" ),
$.ajax( "/modules.php" ),
$.ajax( "/lists.php" )
).then(successFunc, failureFunc);
~~~
上面代碼表示,要等到三個ajax操作都結束以后,才執行then方法指定的回調函數。
when方法里面要執行多少個操作,回調函數就有多少個參數,對應前面每一個操作的返回結果。
~~~
$.when(
$.ajax( "/main.php" ),
$.ajax( "/modules.php" ),
$.ajax( "/lists.php" )
).then(function (resp1, resp2, resp3){
console.log(resp1);
console.log(resp2);
console.log(resp3);
});
~~~
上面代碼的回調函數有三個參數,resp1、resp2和resp3,依次對應前面三個ajax操作的返回結果。
如果when方法的參數不是deferred或promise對象,則直接作為回調函數的參數。
~~~
d = $.Deferred()
$.when(d, 'World').done(function (s1, s2){
console.log(s1);
console.log(s2);
})
d.resolve('Hello')
// Hello
// World
~~~
上面代碼中,when的第二個參數是一個字符串,則直接作為回調函數的第二個參數。
此外,如果when方法的參數都不是deferred或promise對象,那么when方法的回調函數將立即運行。
## 使用實例
### wait方法
我們可以用deferred對象寫一個wait方法,表示等待多少毫秒后再執行。
~~~
$.wait = function(time) {
return $.Deferred(function(dfd) {
setTimeout(dfd.resolve, time);
});
}
~~~
使用方法如下。
~~~
$.wait(5000).then(function() {
console.log("Hello from the future!");
});
~~~
### 改寫setTimeout
在上面的wait方法的基礎上,還可以改寫setTimeout方法,讓其返回一個deferred對象。
~~~
function doSomethingLater(fn, time) {
var dfd = $.Deferred();
setTimeout(function() {
dfd.resolve(fn());
}, time || 0);
return dfd.promise();
}
var promise = doSomethingLater(function (){
console.log( '已經延遲執行' );
}, 100);
~~~
### 自定義操作使用deferred接口
我們可以利用deferred接口,使得任意操作都可以用done()和fail()指定回調函數。
~~~
Twitter = {
search:function(query) {
var dfd = $.Deferred();
$.ajax({
url:"http://search.twitter.com/search.json",
data:{q:query},
dataType:'jsonp',
success:dfd.resolve
});
return dfd.promise();
}
}
~~~
使用方法如下。
~~~
Twitter.search('javaScript').then(function(data) {
alert(data.results[0].text);
});
~~~
deferred對象的另一個優勢是可以附加多個回調函數。下面的例子使用了上面所改寫的setTimeout函數。
~~~
function doSomething(arg) {
var dfd = $.Deferred();
setTimeout(function() {
dfd.reject("Sorry, something went wrong.");
});
return dfd;
}
doSomething("uh oh").done(function() {
console.log("Won't happen, we're erroring here!");
}).fail(function(message) {
console.log(message);
});
~~~
## 參考鏈接
* Matt Baker,?[jQuery.Deferred is the most important client-side tool you have](http://eng.wealthfront.com/2012/12/jquerydeferred-is-most-important-client.html)
* [Fun With jQuery Deferred](http://www.intridea.com/blog/2011/2/8/fun-with-jquery-deferred)
* Bryan Klimt,?[What’s so great about JavaScript Promises?](http://blog.parse.com/2013/01/29/whats-so-great-about-javascript-promises/)
* José F. Romaniello,?[Understanding JQuery.Deferred and Promise](http://joseoncode.com/2011/09/26/a-walkthrough-jquery-deferred-and-promise/)
* Julian Aubourg, Addy Osmani,?[Creating Responsive Applications Using jQuery Deferred and Promises](http://msdn.microsoft.com/en-us/magazine/gg723713.aspx)
* Graham Jenson,?[JQuery Promises and Deferreds: I promise this will be short](http://maori.geek.nz/post/i_promise_this_will_be_short)
* Q module document,?[Coming from jQuery](https://github.com/kriskowal/q/wiki/Coming-from-jQuery)
- 第一章 導論
- 1.1 前言
- 1.2 為什么學習JavaScript?
- 1.3 JavaScript的歷史
- 第二章 基本語法
- 2.1 語法概述
- 2.2 數值
- 2.3 字符串
- 2.4 對象
- 2.5 數組
- 2.6 函數
- 2.7 運算符
- 2.8 數據類型轉換
- 2.9 錯誤處理機制
- 2.10 JavaScript 編程風格
- 第三章 標準庫
- 3.1 Object對象
- 3.2 Array 對象
- 3.3 包裝對象和Boolean對象
- 3.4 Number對象
- 3.5 String對象
- 3.6 Math對象
- 3.7 Date對象
- 3.8 RegExp對象
- 3.9 JSON對象
- 3.10 ArrayBuffer:類型化數組
- 第四章 面向對象編程
- 4.1 概述
- 4.2 封裝
- 4.3 繼承
- 4.4 模塊化編程
- 第五章 DOM
- 5.1 Node節點
- 5.2 document節點
- 5.3 Element對象
- 5.4 Text節點和DocumentFragment節點
- 5.5 Event對象
- 5.6 CSS操作
- 5.7 Mutation Observer
- 第六章 瀏覽器對象
- 6.1 瀏覽器的JavaScript引擎
- 6.2 定時器
- 6.3 window對象
- 6.4 history對象
- 6.5 Ajax
- 6.6 同域限制和window.postMessage方法
- 6.7 Web Storage:瀏覽器端數據儲存機制
- 6.8 IndexedDB:瀏覽器端數據庫
- 6.9 Web Notifications API
- 6.10 Performance API
- 6.11 移動設備API
- 第七章 HTML網頁的API
- 7.1 HTML網頁元素
- 7.2 Canvas API
- 7.3 SVG 圖像
- 7.4 表單
- 7.5 文件和二進制數據的操作
- 7.6 Web Worker
- 7.7 SSE:服務器發送事件
- 7.8 Page Visibility API
- 7.9 Fullscreen API:全屏操作
- 7.10 Web Speech
- 7.11 requestAnimationFrame
- 7.12 WebSocket
- 7.13 WebRTC
- 7.14 Web Components
- 第八章 開發工具
- 8.1 console對象
- 8.2 PhantomJS
- 8.3 Bower:客戶端庫管理工具
- 8.4 Grunt:任務自動管理工具
- 8.5 Gulp:任務自動管理工具
- 8.6 Browserify:瀏覽器加載Node.js模塊
- 8.7 RequireJS和AMD規范
- 8.8 Source Map
- 8.9 JavaScript 程序測試
- 第九章 JavaScript高級語法
- 9.1 Promise對象
- 9.2 有限狀態機
- 9.3 MVC框架與Backbone.js
- 9.4 嚴格模式
- 9.5 ECMAScript 6 介紹
- 附錄
- 10.1 JavaScript API列表
- 草稿一:函數庫
- 11.1 Underscore.js
- 11.2 Modernizr
- 11.3 Datejs
- 11.4 D3.js
- 11.5 設計模式
- 11.6 排序算法
- 草稿二:jQuery
- 12.1 jQuery概述
- 12.2 jQuery工具方法
- 12.3 jQuery插件開發
- 12.4 jQuery.Deferred對象
- 12.5 如何做到 jQuery-free?
- 草稿三:Node.js
- 13.1 Node.js 概述
- 13.2 CommonJS規范
- 13.3 package.json文件
- 13.4 npm模塊管理器
- 13.5 fs 模塊
- 13.6 Path模塊
- 13.7 process對象
- 13.8 Buffer對象
- 13.9 Events模塊
- 13.10 stream接口
- 13.11 Child Process模塊
- 13.12 Http模塊
- 13.13 assert 模塊
- 13.14 Cluster模塊
- 13.15 os模塊
- 13.16 Net模塊和DNS模塊
- 13.17 Express框架
- 13.18 Koa 框架