2012年11月29日 星期四

[宅] 令我老淚縱橫的deferred物件

昨天參加完Ruby tuesday後不只解決了困擾已久的環境問題,還在聊天中得知了一個超好用的東西,就是jquery中的deferred物件.....雖然我LAG了很久但秉持著活到老學到老的精神,我還是要給他不知廉恥的寫下去。


事情是從jquery 1.5版開始的....


"老實說,我並不知道1.5版發生了什麼事情,但當我回過神來的時候,jquery的ajax函式已經開始回傳deferred物件了...."


jquery.Deferred(),或稱作$.Deferred(),是一個function,執行後會回傳一個Deferred物件:
Deferred: object
-->resolve: function
-->resolveWith: function
-->reject: function
-->rejectWith: function
-->notify: function
-->notifyWith: function
-->state: function
-->always: function
-->then: function
-->promise: function
-->pipe: function
-->done: function
-->fail: function
-->progress: function

這個物件和Ajax所回傳的object長得不太一樣(以下是get error的狀態):
deferred: object
-->abort: function
-->always: function
-->complete: function
-->done: function
-->error: function
-->fail: function
-->getAllResponseHeaders: function
-->getResponseHeader: function
-->pipe: function
-->progress: function
-->promise: function
-->readyState: 0
-->responseText: ""
-->setRequestHeader: function
-->state: function
-->status: 0
-->statusCode: function
-->statusText: "error"
-->success: function
-->then: function
黑字的部分都是deferred物件的function,但是明顯看得出比原生的deferred物件少了許多,我們姑且稱這是"閹割版"的deferred object,後面會再提到閹割版的由來。

用Ajax的結果來執行callback

Ajax回傳的物件中的"done"、"fail"函式,可以依成功與否來執行傳入的callback function,而很方便的是,這些函式是可以chain起來的。
$.get("http://google.com/").done(function(){alert("YES!!!");}).fail(function(){alert("NO!!!");});
這樣一來,成功的話就會alert "YES",失敗就alert "NO",而上面的寫法也可以用then()來做簡化:
$.get("http://google.com/").then(
    function(){alert("YES!!!");}, //成功時的callback
    function(){alert("NO!!!");} //失敗時的callback
)
但其實Ajax中本來就可以傳入"success"和"error"的callback function,所以光是上面的應用除了讓程式碼的可讀性變得更高也更好寫(當需要error callback時不用刻意改寫成ajax,仍舊可以使用get, getJSON, post)以外,似乎還是有點弱。
接著我們要介紹的是能讓deferred發揮得更強大的$.when()函式


用$.when( )解決"一堆"非同步AJAX的相依性問題

很多時候我們的程式碼會需要依賴數個AJAX回傳的資料來進行運算,以往我們可能只能用下列兩種解法:

  1. 改寫成同步AJAX,也就是一個一個接著執行
  2. 設一個值等於要AJAX次數的counter,每完成一個AJAX就減一,直到counter歸零就執行相依性的函式

但是第一種solution效率太差,第二種又太醜。這時我們就可以使用$.when( )
一般時候使用$.when()跟直接使用deferred物件是差不多的,因為$.when()也是回傳一個deferred物件:
var def = $.when($.ajax("test.html")); //deferred物件
$.when()真正強悍的地方在於能將數個AJAX的結果包成一個deferred物件,只要有一個失敗,那這個deffered的狀態就是"error",這樣講很抽象,我們直接看例子:
$.when( $.get("test1.html"), 
        $.post("test2.php"), 
        $.get("test3.html")
).then( doneCallback,
        failCallback    )
上面的三個AJAX只要有一個失敗了就會觸發errorCallback,三個都成功才會觸發successCallback。
這樣的寫法乾淨許多,可讀性也高。
延伸:AJAX的success callback與deferred的done callback 的執行順序
javascript是單執行緒的語言,但是常常會因為AJAX或wait這些常用的funciton讓初學者有所誤會。如果是非同步的情形,AJAX在等待遠端回覆的過程中,會直接往下執行,直到遠端回應了才會觸發success或error callback。
但是在同一時間javascript是只會做一件事的。所以在上面提到的例子中,如果數個AJAX有傳入success callback,當成功接收到respond時,AJAX會先呼叫success callback,就看哪個AJAX先收到respond而已。而因為javascript單執行緒的特性,在沒有ajax,timer,alert之類的情形下他會完完整整的把一個success callback執行完才執行下一個AJAX的callback。
所以上面的例子中,將會是三個AJAX的callback在不可預期的順序下執行完畢後,才會由$.when()回傳一個狀態為成功的deferred來觸發done callback function。不必擔心done callback有可能比AJAX的callback先被處理。


deferred不只能用在AJAX

如果其他函式也是非同步且具有相依性的需求,例如wait(),我們也可以利用deferred的設計思維來做相依性的處理。但是因為wait()並不回傳deferred物件,所以我們要再把這些函式包起來變成一個回傳deferred物件的函式。舉個例子:(轉載自阮一峰大大的部落格)
var wait = function(dtd){
  var tasks = function(){
    alert("执行完毕!");
    dtd.resolve(); // 改变Deferred对象的执行状态
  };

  setTimeout(tasks,5000);
  return dtd.promise(); // 返回promise对象
};
var d = wait(dtd); // 新建一个d对象,改为对这个对象进行操作
$.when(d)
.done(function(){ alert("哈哈,成功了!"); })
.fail(function(){ alert("出错啦!"); });
d.resolve(); // 此时,这个语句是无效的
這個例子裡有兩個東西要注意,一個是"resolve()"、一個是"promise()"。
resolve()可以將deferred物件改為"成功"狀態並觸發done callback,相反的就是reject了。
promise()能將deferred物件"閹割"成文章最前面所呈現的樣子,這樣的物件沒有辦法再用resolve或reject來更改狀態,顧名思義就是"承諾"(promise)