almost 5 years ago

現在寫javascript常常會有很多非同步(asynchronous)的程式要處理,如果只有一個非同步的程式,在處理successfailure時應該還算是簡單的。最簡單的範例如下:

$.ajax({
    url:'/abc.php',
    success: function () {
        $('#success').show();
    }, 
    error: function (jqXHR, textStatus, errorThrown) {
        console.log(errorThrown);
    }
);

successfailure(error)在處理上相當的直覺簡單。但如果要同時處理多個非同步的successfailure,例如:「等待三個ajax處理完成,統一做一個alert。」這樣子的案例在處理上就會變得相當麻煩。

如果要等待多個非同步的程式執行完成才做一些動作,在jQuery 1.5後的版本可使用 $.when 這個方法。上面說的案例「等待三個ajax處理完成,統一做一個alert。」可以這樣寫:

$.when($.ajax('abc.php'), $.ajax('abc2.php'), $.ajax('abc3.php'))
.done(function(result1, result2, result3) {
    alert('success');
}).fail(function (jqXHR, textStatus, errorThrown) {
    console.log('error');
});

這樣統一的處理,看起來簡單,也省去很多麻煩事。$.when可以用在jQuery的$.ajax$.get$.post...等很多jquery內建的方法上,都不會有問題。因為 $.ajax 本身會回傳一個 $.Deferred 物件,$.Deferred 是一種可串接的物件,主要可以用來把callback丟到callback佇列中,進而統一管理callback,所以jquery後來的文件也建議 $.ajax 改用跟上面 $.when 相同的寫法:

$.ajax('abc.php').done(function(result) {
    alert('success');
}).fail(function (jqXHR, textStatus, errorThrown) {
    console.log('error');
});

但是其他的非jQuery的程式,例如:WebSQL的executeSql,在使用 $.when 時,可能就會有問題出現了,jQuery官方文件有寫到:

If a single argument is passed to jQuery.when and it is not a Deferred or a Promise, it will be treated as a resolved Deferred and any doneCallbacks attached will be executed immediately. 

意思是說如果傳進 $.when 的參數不是一個 DeferredPromise 物件,將會被視為已成功執行(resolved),而且所有的doneCallback都會馬上被執行。所以程式根本不會等到非同步程式執行完,才執行done的程式,而是「馬上」執行!既然這樣,那我們只好把非jquery的非同步程式包成一個 $.Deferred 物件來讓 $.when 可以正常的執行。

$.Deferred 的概念其實蠻簡單的,$.Deferred 裡有個狀態的flag,當物件剛宣告時,是 pending,成功執行時,可以呼叫 deferred.resolve() 將狀態改為 resolved。反之,可以呼叫 deferred.reject() 將狀態改為 rejected,讓程式去呼叫fail。簡而言之,$.when 就是利用這個狀態的flag來判斷該執行done還是fail,或是其他callback。所以只要將狀態管理好即可。

PromiseDeferred 相當類似,只是 Promise 少了設定狀態的方法,ex.resolve、reject...等。jQuery官方建議只return promise物件,以防止其他程式變更狀態。

接下來看看範例,可以宣告一個 $.Deferred 物件,再success時用程式去呼叫 resolve 即可,如下:

var insertToDB = function () {
    var deferred = $.Deferred();   //宣告Deferred物件

    var dbSize = 1024 * 1024;
    var db = openDatabase("db1", "1", "", dbSize);
    db.transaction(function(tx){
        tx.executeSql("INSERT INTO table1(col1, col2) VALUES (?,?)", ['value1', 'value2'],
            function () { 
                console.log('insert successed');
                deferred.resolve();   //update state

            },
            function (tx, error) {
                console.log('error:' + error);
                deferred.reject();    //update state

            });
   }); 
   
   return deferred.promise();  
}

//execute

$.when(insertToDB()).then(function () {
  alert('when complete');
});

另外,也可以利用 $.Deferred 的beforeStart去完成 $.Deferred 的實作,範例如下:

var dbAccessor = $.Deferred(function(deferred) {
    var dbSize = 1024 * 1024;
    var db = openDatabase("db1", "1", "", dbSize);
    db.transaction(function(tx){
        tx.executeSql("INSERT INTO table1(col1, col2) VALUES (?,?)", ['value1', 'value2'],
            function () { 
                console.log('insert successed');
                deferred.resolve();   //update state

            },
            function (tx, error) {
                console.log('error:' + error);
                deferred.reject();    //update state

            });
   }); 
});

//execute

$.when(dbAccessor).then(function () {
  alert('when complete');
});

參考資料

  1. http://api.jquery.com/category/deferred-object/
  2. http://joseoncode.com/2011/09/26/a-walkthrough-jquery-deferred-and-promise/
  3. http://danieldemmel.me/blog/2013/03/22/an-introduction-to-jquery-deferred-slash-promise/
← ubuntu vsftpd 安裝筆記 用CSS畫三角形 →
 
comments powered by Disqus