Deferred首次出現在jQuery 1.5中,在jQuery 1.8之后被改寫,它的出現抹平了javascript中的大量回調產生的金字塔,提供了異步編程的能力,它主要服役於jQuery.ajax。
Deferred就是讓一組函數在合適的時機執行,在成功時候執行成功的函數系列,在失敗的時候執行失敗的函數系列,這就是Deferred的本質。簡單的說,模型上可以規划為兩個數組來承接不同狀態的函數——數組resolve里的函數列表在成功狀態下觸發,reject中的函數在失敗狀態下觸發。
這篇文章主要分為以下知識,和上一篇博文《讀jQuery源碼之 - Callbacks》關聯。
什么是Deferred
初窺Deferred
Deferred本身就是承接一組函數,在異步中執行這組函數,過去的我們是這樣編寫異步代碼的:

var resolve = function () { console.log('成功'); }, //定義一個失敗狀態下運行的函數 reject = function () { console.log('失敗'); }; //模擬服務器請求,正在等待服務器響應ing..... setTimeout(function (status) { if (status === 200) resolve(); else reject(); }, 1000);
如果用Deferred那么上面的代碼則應該是下面這個樣子:

var deferred = new Deferred(); //先將成功和失敗的函數委托到Deferred deferred.resolve(function () { console.log('成功'); }).reject(function () { console.log('失敗'); }).resolve(function () { console.log('我還想再追加一個成功的函數'); }); //當改變狀態的時候,會自動觸發成或者失敗的函數 setTimeout(function (status) { if (status === 200) deferred.resolve(); else deferred.reject(); });
Deferred有點Callbacks的特質,不過是Callbacks的逼格提升版(異步定制版)。在Callbacks的基礎上提升了對於異步函數的管理——本身的使用和Callbacks一樣:承接一組函數,觸發執行。
Deferred主要服役於jQuery.ajax(),使用了Deferred的ajax代碼如下:
//Deferred的應用(ajax) $.ajax('demo.html').done(function () { console.log('ajax成功'); }).fail(function () { console.log('ajax失敗'); }).done(function () { console.log('ajax成功,追加一條函數處理我們自己的事情...'); });
jQuery把Deferred對象封裝到ajax中,jQuery.ajax()中,jQuery維護ajax請求的發起到接收,而使用jQuery的開發者,只關注ajax的結果即可,眾所周知,ajax是異步的,而我們這些成功后(失敗后)要執行的函數,從代碼的層面上,是線性的編寫的——正是Deferred提供了這樣異步編程的能力。
回調函數的定義(委托)和回調函數的執行
Deferred切割了回調函數和執行時機兩個概念。就是把回調函數的定義和回調函數的執行這兩個概念給分離開,同一時間專注一個概念,這樣代碼就能夠線性的編寫下去,Deferred主要應用於這種回調函數的多層嵌套,而這種情況多發生於異步(當然它也確實是為異步量身打造),所以就叫Deferred——讓你異步的代碼,看起來跟像同步執行一樣。當然它並不局限與異步。
Deferred在Callbacks基礎上做的二次封裝,它封裝了一組狀態,每組狀態對應一個Callbacks對象。我們還是說的再簡單通俗一點吧:Deferred主要有三個狀態作為工作標志:成功、失敗、無狀態。成功失敗還好點,這個“無狀態”是個神馬意思??
Deferred本身就是根據狀態來觸發的,成功狀態下觸發成功狀態的函數,失敗狀態下觸發失敗狀態的函數,最后這個無狀態就是:既不成功,也不失敗,但是每次要觸發相應的函數——用於文件上傳,在文件上傳的ajax中,要和服務器一直保持請求,每次請求既不代表成功也不代表失敗,那么這個無狀態就是最好的標志。
Deferred本質上的實現就是用數組專門用來存放對應狀態的函數,然后循環執行。就是有三個數組:代表成功狀態下執行的resolve函數數組和代表失敗狀態下執行的reject函數數組, 還有一個每次觸發都會執行的progress數組。
jQuery.Deferred的Promise
jQuery.Deferred里面實現了Promise/A規范。
jQuery中,jQuery.Deferred其實本身就已經實現了Promise/A規范,並且還擴充了一套很實用的API,但是jQuery.Deferred對象中又包含着一個Promise對象,這個對象和Promise/A基本沒有關聯,它是切掉丁丁的jQuery.Deferred(閹割版),只有上面所說的回調函數的定義這一部分API,並沒有回調函數的執行這一部分的API,這么做是因為可以在Ajax中把回調函數的執行給封閉起來(jQuery自己維護這部分),而使用jQuery的開發者則使用回調函數的定義這部分——實現一個恰到好處的觀察者模式。
Deferred工作在Ajax更深層次的地方,而外層只需要根據相關結果做出對應的操作即可,即Promise對象。例如Ajax請求成功后,Deferred工作在Ajax內層(發送請求,接收請求) ,Promise對外的API接收了對應行為的函數,當內層Ajax請求成功的時候,通過Deferred標識狀態為成功,那么這些承接的函數都會執行——Deferred主要應用於這樣的工作場景。而因為對外開放的是Promise,它並不具備回調函數的執行這部分代碼,而這部分,是被內層Deferred維護的。
這就類似你們大boss要你辦一件事,並提前給了三種情形的解決方案,分別表示:這件事處理成功了之后該怎么做,處理失敗了又該怎么做,處理中該怎么做。大boss給的解決方案,就是回調函數的定義,而你在這件事得到結果后針對不同的情況進行處理,就是回調函數的執行,當然,大boss也可以自己來處理這件事。並在得到結果后自己針對不同的情況進行處理,這就是Deferred和Promise對象的關系於用途。
Deferred的模型與工作原理
上面嘰里呱啦說了一大堆,仍然沒懂?木關系,我們直接來看模型看API就能確定這玩意兒到底是什么了,jQuery.Deferred有如下API:
API | 隸屬對象 | 描述 | 實現 |
---|---|---|---|
done(function[,function...]) | Deferred&Promise | 添加一個或多個表示成功的函數到Deferred對象中,與Deferred.resolve()方法對應。 | 內部原型是Callbacks對象,該方法直接引用Callbacks.add() |
fail(function[,function...]) | Deferred&Promise | 添加一個或多個表示失敗的函數到Deferred對象中,與Deferred.reject()方法對應。 | 同上 |
progress(function[,function...]) | Deferred&Promise | 添加一個或多個表示無狀態的函數到Deferred對象中,與Deferred.notify()方法對應,每次執行Deferred.notify()都會執行委托的回調函數,而done()、fail()方法中委托的回調函數都是一次性的。 | 同上 |
resolve([args]) | Deferred | 觸發成功系列函數(通過Deferred.done()追加的函數),注意每次執行這些函數之后都會被銷毀。 | 內部原型直接引用了Callbacks對象的fireWith()方法。 |
reject([args]) | Deferred | 觸發失敗系列函數(通過Deferred.fail()追加的函數),注意每次執行這些函數之后都會被銷毀。 | 同上 |
notify([args]) | Deferred | 觸發無狀態系列函數(通過Deferred.progress()追加的函數),注意每次執行這些函數之后都會被銷毀。 | 同上 |
promise([Object]) | Deferred&Promise | 無參的情況下返回Promise對象,有參數的情況下為參數Object擴展Promise行為。 | 閹割版的Deferred,內部先定義了Promise的基礎API,在此基礎上擴展了Deferred,就是用有參的promise()將promise的行為擴展到Deferred上的。 |
state() | Deferred&Promise | 返回當前狀態的字符串:pending(尚未執行)、resolved(已成功)、rejected(已失敗)、undefined(無狀態,未定義) | 執行相應函數的時候標識一下狀態就可以了。 |
then(doneCallbacks[,failCallbacks[,progressCallbacks]]) | Deferred&Promise | 在jQuery 1.8以后被重寫,委托最多三組函數到Deferred對象中,分別表示:成功、失敗、無狀態下執行的函數,從使用上來說,是Deferred.done()、Deferred.fail()、Deferred.progress()的簡寫版——然而,本質上並非如此,then方法是單獨實現的——它返回一個全新的Promise對象,它連接了鏈式回調中的參數,讓每個函數都可以與上一層、下一層函數通信,詳情請見《jQuery.Deferred的then()》小節。 | 內部的實現較為復雜,創建了一個全新的Deferred對象(與Deferred.done())系列函數完全不同,每一次在同一個Deferred對象上鏈式調用then()都建立了深層的嵌套,並且通過回調函數的返回值與下一層進行通信。 |
always(function[,function]) | Deferred&Promise | 接收兩個函數,分別表示成功、失敗執行的函數,這才是正統的使用Deferred.done()、Deferred.fail()實現的API。 | 內部調用Deferred.done()、Deferred.fail()實現 |
other | Deferred&Promise | 還有一些其他的API無關痛癢啊,基本都是在上面的API基礎上擴展的,so easy~~~ |
從上面的API里可以看見,Promise對象就是切掉小丁丁版本的Deferred,只有回調函數的定義(done/fail/progress)API,沒有回調函數的執行(resolve/reject/notify)API。下面有美圖一張...
基礎部分從代碼的表現上(API的使用上)是這些:

(function () { //Deferred的done/resolve var deferred = $.Deferred(); deferred.done(function (state) { console.log(state); //write 1 }); resolve.resolve(1); } ()); ~function () { //Deferred的fail/reject var deferred = $.Deferred(); deferred.fail(function (state) { console.log(state); //write 2 }); resolve.reject(2); } (); !function () { //Deferred的progress/notify /* progress/notify對應的是“無狀態”的狀態 它表示一個既不表示成功,也不表示失敗的狀態 它每一次的觸發(notify)都會執行progress里面的函數 和resolve、reject不同,通過progress委托的函數,每次notify都不會被清空 它可以反復被執行,用於會話保持 */ var deferred = $.Deferred(); progress.progress(function (state) { console.log(state); //write 2 }); setTimeout(function () { //每隔1s反復執行 resolve.notify(2); }, 1000); } ();
jQuery.Deferred的實現
上面說了一大堆概念啊神馬的可能好多人都覺得這他瞄的什么玩意兒,直接給個痛快咱們看代碼吧。
那就亮好我們的12氪鈦金硬化寫輪防暴+12透視*2000狗眼:
結構:

//jQuery.Deferred結構代碼 jQuery.extend({ Deferred: function (func) { var tuples = [ ["resolve", "done", jQuery.Callbacks("once memory"), "resolved"], ["reject", "fail", jQuery.Callbacks("once memory"), "rejected"], ["notify", "progress", jQuery.Callbacks("memory")] ], state = "pending", promise = { state: function () { return state; }, always: function () { //直接調用 deferred.done(arguments).fail(arguments); return this; }, //then方法稍后解讀 then: function ( /* fnDone, fnFail, fnProgress */) { }, promise: function (obj) { return obj != null ? jQuery.extend(obj, promise) : promise; } }, deferred = {}; //過去pipe,現在的then promise.pipe = promise.then; jQuery.each(tuples, function (i, tuple) { var list = tuple[2], stateString = tuple[3]; promise[tuple[1]] = list.add; //內部先壓入三個函數 if (stateString) { list.add(function () { state = stateString; //把互斥的函數和無狀態函數都給禁用掉 }, tuples[i ^ 1][2].disable, tuples[2][2].lock); } //resolve/reject/notify deferred[tuple[0]] = function () { //這里的this===deferred為什么做這一層判定沒有理解 //這些觸發狀態的方法只能是deferred擁有,既然是deferred的觸發,那么為何又要閹割當前上下文呢? deferred[tuple[0] + "With"](this === deferred ? promise : this, arguments); return this; }; //resolveWith/rejectWith/notifyWith deferred[tuple[0] + "With"] = list.fireWith; }); //promise有參方法是擴展這個參數 promise.promise(deferred); //配合then使用的 if (func) { func.call(deferred, deferred); } return deferred; } });
對於基礎的API實現[ done/fail/progress | resolve/reject/notify],jQuery把這一部分的代碼抽離出來,在后面采用循環一次性動態生成的方式實現。
var tuples = [ ["resolve", "done", jQuery.Callbacks("once memory"), "resolved"], ["reject", "fail", jQuery.Callbacks("once memory"), "rejected"], ["notify", "progress", jQuery.Callbacks("memory")] ]
首先實現的promise,前面說了,promise是切掉小丁丁版本的Deferred,所以先實現promise,后面把它的API擴展到Deferred里面即可。
promise = { state: function () {}, always: function () {}, then: function ( /* fnDone, fnFail, fnProgress */) { }, promise: function (obj) { //有參的它為了這個參數擴展了promise行為 return obj != null ? jQuery.extend(obj, promise) : promise; } }
注意這個promise()方法的實現,無參的它把Promise對象的行為擴展到Deferred,后面就直接用這個方法擴展Deferred就可以讓Deferred對象擁有promise的API了。
在前面准備工作完畢了之后,生成通用的部分,直接循環上面定義的通用數組,直接把Callbacks對象相應的方法引用到API上,因為我們之前Callbacks內部的實現,最終返回的都是this,這里直接引用過去之后,this就代表了Deferred/Promise對象,仍然支持鏈式回調。
在循環中,這里的代碼很是心思慎密:
jQuery.each(tuples, function (i, tuple) { var list = tuple[2], stateString = tuple[3]; promise[tuple[1]] = list.add; //內部先壓入三個函數 if (stateString) { list.add(function () { state = stateString; //把互斥的函數和無狀態函數都給禁用掉,i^1位運算,跑下控制台就知道了 }, tuples[i ^ 1][2].disable, tuples[2][2].lock); } //resolve/reject/notify deferred[tuple[0]] = function () { //這里的this===deferred為什么做這一層判定沒有理解 //這些觸發狀態的方法只能是deferred擁有,既然是deferred的觸發,那么為何又要閹割當前上下文呢? deferred[tuple[0] + "With"](this === deferred ? promise : this, arguments); return this; }; //resolveWith/rejectWith/notifyWith deferred[tuple[0] + "With"] = list.fireWith; });
stateString取值范圍(上面數組的定義中)有三個:"resolved"、"rejected"、undefined。
所以進入這個判定之后,變量i的值只可能是0||1。
然后給Callbacks中壓入三個響應的回調函數,分別執行了修改狀態字符串、將互斥的函數設置為不可用、鎖定無狀態的函數,后兩個直接引用了Callbacks的方法。這里:通過位運算符得到互斥的索引,然后根據索引訪問上面數組里對應的Callbacks,直接禁用和鎖定。
也就是說,Deferred默認為每個狀態壓入了三個函數,當我們使用done/fail/progress的時候,是在這三個函數之后執行的,當首次執行觸發狀態函數(resolve/reject/notify),先執行了這三個函數,再來美圖一張,演示了Deferred整個內部模型:
后面的代碼,木有了!!!!你木有看錯,是真的木有了!!!有這么一點點啊!真的就這么一點點代碼!!!有木有感覺so easy?隨手就寫了一個Deferred有木有啊?!
小伙雞,還有一個大塊頭呢,不要忽略這個API——Deferred.then()!
jQuery.Deferred的then()
這個then()啊,很是巧妙,讀起來就簡直就是各種痛經啊。我們先再來詳細擼一發then()的定義。
then的定義:
Promise和Deferred共同擁有API——then():上面的源碼里可以看見,Deferred里面本質上是三個Callbacks在工作,,分別存放着不同狀態下都要執行的函數列表,看過別人的解釋:如果我們添加一個成功狀態下要執行的函數,那么大家可能想着調用Deferred.done()。而then()呢,提供了一個便捷的API,then()接收三個參數,分別表示:成功狀態下執行的函數,失敗狀態下執行的函數,每次觸發狀態下執行的函數——其實意思上就是把done/fail/progress合並到了一個API。
嗯,這是在jQuery.1.8以前then()的實現,在jQuery.1.8以前,then()只是一個普通的實現,1.7.2中它的實現:
then: function (doneCallbacks, failCallbacks, progressCallbacks) { deferred.done(doneCallbacks).fail(failCallbacks).progress(progressCallbacks); return this; }
可以看見只是就是直接調用了Call自己的API啊,真真正正的提供了便捷的API入口。我們還是擼一下then的前世吧,在jQuery.1.8以前,有一個APIDeferred.pipe():這API的作用是:提供一個類似always()的API,也就是三個參數,分別表示done、fail、progress狀態的函數,也就是把這個三個API合並到一起了,同時,這些函數都可以溝通。
jQuery.1.8以后,Deferred.pipe()過時,取代它的API就是Deferred.then()。
什么叫做這些函數可以溝通?看如下代碼(Deferred.then):

//普通的應用 !(function () { var deferred = $.Deferred(); deferred.done(function (value) { return value * 10; }) .done(function (value) { console.log(value); }); deferred.resolve(1); //result ---- 1 })(); //then的應用 !(function () { var deferred = $.Deferred(); deferred.then(function (value) { return value * 10; }).then(function (value) { console.log(value); }); deferred.resolve(1); //result ---- 10 })();
通過then()添加的函數,同一狀態下,上一個函數的返回值可以傳遞到下一層,這就是then/pipe的實現。
Deferred.then:

then: function ( /* fnDone, fnFail, fnProgress */) { //保存參數 var fns = arguments; //注意這里的Deferred,Deferred參數如果是一個函數,那么會直接執行這個函數,參數就是閉包里的deferred對象!! return jQuery.Deferred(function (newDefer) { jQuery.each(tuples, function (i, tuple) { var fn = jQuery.isFunction(fns[i]) && fns[i]; // deferred[ done | fail | progress ] for forwarding actions to newDefer //注意這里已經把then()里面的函數封裝到了上一層deferred對象中 deferred[tuple[1]](function () { var returned = fn && fn.apply(this, arguments); if (returned && jQuery.isFunction(returned.promise)) { //這一層判定的判定擴展了函數返回的Promise/Deferred對象,這里應該是給jQuery.when()方法使用的 returned.promise() .done(newDefer.resolve) .fail(newDefer.reject) .progress(newDefer.notify); //其實這里的擴展,應該只是純粹的對具有promise/A的擴展,只是留了這個功能,什么時候執行,並不是jQuery.Deferred關心的事情 } else { //這是then方法的本質,使用then()返回的promise對象依賴於newDefer對象 //then方法中,這里把上一層的返回值傳遞到下一層 //而現在的環境只能被最頂層的Deferred觸發 //在觸發頂層的Deferred中,觸發then()中的Deferred //這里的判定,為什么要做這一層對象的封裝呢? newDefer[tuple[0] + "With"](this === promise ? newDefer.promise() : this, fn ? [returned] : arguments); } }); }); //這里可以放心釋放fns,在上面的each中,已經單獨創建了對應了變量 fns = null; }).promise(); }
實現上比較饒,做了這些事情:
- 1、創建了一個新的Deferred對象,Deferred對象構造函數里,如果傳入一個函數作為參數,那么這個函數就會立即執行,這個函數的參數和上下文,就是新創建的Deferred對象。
- 2、因為then的API承接done/fail/progress這些函數,所以循環上面定義的那個公共部分的數組,一次循環三個函數一並處理了。
- 3、在每次循環中,創建一個匿名的函數,添加到上一層的Deferred對象中,通過done/fail/progress添加,所以這個函數,會在上一層Deferred對象標志狀態的時候(resolve/reject/notify)被執行,這一步其實是在封裝通過then()添加進來的函數。
- 4、在匿名函數中,執行通過then()添加進來的對應狀態的函數,並獲取到返回值。
- 5、做了一次返回值的判定,如果這個返回值擁有promise/A的行為,則把當前Deferred對象里面所有的函數擴展到這個返回值對象中,注意是當前Deferred,而不是閉包外的Deferred,then中當前Deferred和then之外的Deferred是兩個對象。
- 6、如果這個返回值不具有promise/A的行為,則直接執行當前Deferred對象相應標識狀態的函數(resolve/reject/notify)
這里的代碼如下幾點需要注意:
作用域:newDefer是then中新創建的Deferred對象,then最終返回的是這個對象的Promise,而在這個newDefer中通過done/fail/progress壓入的函數,都是壓在上一層Deferred中,也就是變量deferred。
執行鏈:then中,一開始有參數newDefer的大匿名函數,是在新的Deferred對象里執行的,而在這里面,又通過上一層的變量deferred對應的done/fail/progress添加的匿名函數,在添加的匿名函數里,又調用了newDefer對應的resolve/reject/notify——即上一層驅動了下一層的執行,讀透它需要多一點思考。
手賤又畫了張圖,美圖一張:
思考:
代碼的閱讀:
我讀代碼的時候先讀的Deferred的基礎部分,最后單獨讀then()的,基礎部分通俗易懂,公共的數組和現有API的利用非常巧妙。代碼上,個人覺得我讀的版本jQuery.1.11.1代碼整理的非常精致,但是閱讀起來略感晦澀,讀完了之后個人也讀了jQuery.1.7.2(以前一直用1.7.2的),覺得后者的代碼整理上不如前者,但是相比前者閱讀上更加的通俗易懂和簡單明了。
then:
then是實現很是精髓,尤其要理解每一段代碼會產生的作用,jQuery在then里面有這么一段代碼:
if (returned && jQuery.isFunction(returned.promise)) { returned.promise() .done(newDefer.resolve) .fail(newDefer.reject) .progress(newDefer.notify); }
這里的代碼琢磨了好久為什么,會發生什么,各種代碼模擬嘗試,思考了一下,如果我們委托的函數返回一個具有promise行為的對象,那么這里就提供了停下后續函數的執行這么一個實現,並且,會把當前Deferred對象里所有未執行的函數(done/fail/progress)都傳遞給這個具有promise行為的對象。
then的職責:
為了讓函數的可以溝通,實現了then,而then一直用jQuery.Deferred創建新的實例,這么做主要的作用是每個不同的Deferred.then()他們的溝通是被隔離的,如下代碼:

!function () { //then中上下文被隔離 var deferred = $.Deferred(); deferred.then(function (value) { return value * 10; }); //then創建了新的Deferred deferred.then(function (value) { console.log(value);//還是1,他們的上下文被隔離了 }); deferred.resolve(1); } (); !function () { //then中使用不被隔離的上下文 var deferred = $.Deferred(); deferred.then(function (value) { return value * 10; }).then(function (value) { //在上一個then的返回值上調用,所以上下文沒有被隔離 console.log(value);//10 }); deferred.resolve(1); } ();
如上代碼所示,每個不同的then都被隔離了:Deferred.then和done/fail/progress之間不同的地方就是讓每個相同狀態的函數都可以溝通(通過返回值),而每個Deferred.then(每次獨立調用Deferred.then)之間的不同則會讓作用域被隔離。
我思考過then的重構:嘗試着把每個函數都可以溝通這個概念直接應用在done/fail/progress上面,這樣就不用在then中一直創建新的Deferred對象,但是這么做就讓done/fail/progress承載了太多,每次對函數執行后的返回值做判定,這根本就是不靠譜的做法,並且失去了上面所說的隔離溝通,看起來好像Deferred更加的平滑了,其實這樣會讓Deferred應用條件變得更加苛刻。反觀jQuery.Deferred,每一個API做的不多,但是足夠細膩與精准,只能說我還是圖樣圖森破啊...
每個函數都有自己的職責,不要讓它承載的太多,太多的職責決定了這個函數會越發的不可控。我想,這或許也是jQuery.then單獨實現的一個理由吧。
類,往往因為承載的太多而變得臃腫不堪。
最后,我手抄了一份jQuery.Deferred的代碼,可以單獨運行,並加入了注釋。