jQuery Deferred對象

什麼是deferred對象

開發網站的過程當中,咱們常常遇到某些耗時很長的javascript操做。其中,既有異步的操做(好比ajax讀取服務器數據),也有同步的操做(好比遍歷一個大型數組),它們都不是當即能獲得結果的。javascript

一般的作法是,爲它們指定回調函數(callback)。即事先規定,一旦它們運行結束,應該調用哪些函數。html

簡單說,deferred對象就是jQuery的回調函數解決方案。在英語中,defer的意思是"延遲",因此deferred對象的含義就是"延遲"到將來某個點再執行。java

它解決了如何處理耗時操做的問題,對那些操做提供了更好的控制,以及統一的編程接口。它的主要功能,能夠歸結爲四點。jquery

ajax的鏈式寫法

咱們來看一個$.ajax請求的例子:ajax

$.ajax({
    url: 'test.jspx',
    success: function(data) {
        // 請求成功了
        // TODO
    },
    error: function(error) {
        // 請求失敗了
        // TODO
    }
});

上面的代碼再常見不過了,不過下面的寫法你也必定看過:編程

$.ajax('test.jspx')
    .done(function(data) {
        // 請求成功了
        // TODO
    })
    .fail(function(error) {
        // 請求失敗了
        // TODO
    });
    
$.ajax({
    url: 'test.jspx',
    type:'POST',
    data: data
}).done(function(data) {
    // 請求成功了
    // TODO
}).fail(function(error) {
    // 請求失敗了
    // TODO
});

其實在1.5.0版本的以前的jquery是不支持鏈式寫法的,只能使用第一種寫法,緣由是此版本以前的jquery的ajax操做返回的是一個XHR (XMLHttpRequest) 對象,這個對象沒有像donefail這樣的回調方法。 數組

以後的版本返回的是一個deferred對象用promise方法包裝過的對象,能夠進行鏈式操做,使用鏈式寫法後,代碼可讀性大大提升。promise

爲一個操做指定多個回調函數

若是要在一個ajax請求成功後再執行別的回調函數,該怎麼辦呢? 直接在加在後面就能夠了:服務器

$.ajax('test.jspx')
    .done(function(data) {
        // 請求成功了
        // TODO
    }).fail(function(error) {
        // 請求失敗了
        // TODO
    }).done(function(data) {
        // 請求成功了
        // then TODO
    });

這種寫法能夠支持無數個回調函數,這寫回調函數將按照添加順序依次執行。異步

爲多個操做指定回調函數

若是一個回調函數須要在幾個ajax請求都成功後才能執行該怎麼辦呢?是否是很差控制呢?其實jQuery提供了這樣一個方法$.when() 它能夠接收任意個deferred對象,只有全部的deferred對象都狀態都爲成功時才執行done回調,不然執行fail回調。

$.when($.ajax("test1.html"), $.ajax("test2.html"))
    .done(function() {
        // 兩個操做都請求都成功
        // TODO
    }).fail(function() {
        // 任意一個失敗
        // TODO
    });

上面代碼中,只有兩個請求都成功後纔會執行done回調,只要有一個失敗就執行fail回調。

給普通的操做指定回調函數

deferred對象的最大優勢,就是它把這一套回調函數接口,從ajax操做擴展到了全部操做。也就是說,任何一個操做----不論是ajax操做仍是本地操做,也不論是異步操做仍是同步操做----均可以使用deferred對象的各類方法,指定回調函數。

var wait = function() {
    var dtd = $.Deferred();     // 新建一個deferred對象
        
    var tasks = function() {      
        alert("執行完畢!");      
        dtd.resolve();          // 此操做完成後改變deferred對象的執行狀態                          
    };      
    setTimeout(tasks, 5000);    
    return dtd;  
};

// 綁定回調函數                 
$.when(wait())
    .done(function() {
        alert("執行成功了!");
    })
    .fail(function() {
        alert("出錯啦!");
    });

上面代碼的中的wait方法模擬了一個很耗時的操做,以後給這個操做指定了回調函數donefail。一旦wait執行完畢就會當即調用done這個回調函數。

Deferred對象的經常使用方法

上面已經對deferred對象有所瞭解了,下面介紹一下deferred對象的經常使用方法。

deferred.resolve()

在jQuery的deferred對象中,規定了它有三種狀態:

未完成 繼續等待

已完成 當即調用done回調

已失敗 當即調用fail回調

resolve方法的做用就是設置deferred對象狀態爲已完成,deferred對象馬上調用done()方法指定的回調函數

deferred.reject()

reject方法的做用是設置deferred對象狀態爲已失敗,deferred對象馬上調用fail()方法指定的回調函數

deferred.done()

done方法用於指定deferred對象狀態爲已完成時的回調函數。

deferred.fail()

done方法用於指定deferred對象狀態爲已失敗時的回調函數。

deferred.then()

then方法接收一到三個參數,分別指定deferred對象狀態爲已成功、已失敗和繼續等待的回調函數。

deferred.always()

always方法用於指定deferred對象狀態爲已成功或已失敗時的回調函數。

即不管這個deferred對象是成功仍是失敗,只要執行完畢都會調用此方法指定的回調。

因爲此方法指定的回調函數的參數是不肯定的(好比ajax請求成功和失敗返回的信息不一樣,成功時爲返回的數據,失敗則爲錯誤信息),最好只使用它的行爲,而不檢查其參數。若是要執行的操做和參數有關,請顯示地指定donefail回調。以下所示:

$.ajax('test1.html')
    .always(function() {
        // 無論請求是否成功,只要請求完畢就執行
        // TODO
        console.log('已請求完畢,狀態未知');
    });

$.ajax('test1.html')
    .done(function(data) {
        // 請求成功時執行
        // TODO
        console.log('請求已成功,返回數據爲:');
        console.log(data);
    })
    .fail(function(error) {
        // 請求失敗時執行
        // TODO
        console.log('請求已失敗,錯誤信息:');
        console.log(error.status, error.statusText);
    });

deferred.progress()

progress方法用於指定deferred對象狀態爲等待中的回調函數。可是它僅在deferred對象生成了進度通知時纔會被調用。

請看下面例子:

var wait = function() {
    var dtd = $.Deferred(); // 新建一個deferred對象
        
    var tasks = function() {      
        alert("執行完畢!");      
        dtd.resolve(); // 此操做完成後改變deferred對象的執行狀態                           
    };    
    setTimeout(tasks, 5000);    
    return dtd;  
};

// 綁定回調函數                 
$.when(wait())
    .done(function() {
        alert("執行成功了!");
    })
    .fail(function() {
        alert("出錯啦!");
    })
    .progress(function(){
        console.log("正在執行中..."); // 此處不會有任何輸出
    });

上面雖然指定了progress回調,可是卻爲沒有任何做用的緣由是因爲在deferred對象沒有生成進度通知,因此其不會被調用。

想要progress回調能執行,須要在deferred對象上調用此回調。notify方法的做用就是根據給定的 args參數 調用Deferred對象上進行中的progress回調。

var wait = function() {
    var dtd = $.Deferred(); // 新建一個deferred對象    
    var i = 1,
        timer,
        percent; // 記錄進度


    var tasks = function() {
        if (i == 11) {
            alert("執行完畢!");
            dtd.resolve(); // 此操做完成後改變deferred對象的執行狀態
        } else {
            percent = (i * 500) / 5000 * 100 + '%';
            dtd.notify(percent); // 調用progress回調
            i++;
            setTimeout(tasks, 500);
        }
    };
    setTimeout(tasks, 1000);
    return dtd;
};
// 綁定回調函數
$.when(wait())
    .done(function() {
        alert("執行成功了!");
    })
    .fail(function() {
        alert("出錯啦!");
    })
    .progress(function(data) {
        console.log('執行中,已完成', data);
    });
// 執行中,已完成 10%
// 執行中,已完成 20%
// 執行中,已完成 30%
// 執行中,已完成 40%
// 執行中,已完成 50%
// 執行中,已完成 60%
// 執行中,已完成 70%
// 執行中,已完成 80%
// 執行中,已完成 90%
// 執行中,已完成 100%
// 以後彈出 執行完畢!和 執行成功了!

這個方法給上傳文件或者耗時操做生成進度條提供了一種可能。

jQuery3.0以上版本對when方法作了大幅調整。向promise/A+靠齊,上面的寫法中notify是觸發不了when中的progress回調的,須要使用promise來給對象部署deferred接口或使用$.Deferred()傳入函數名。

簡而言之,3.0以以上版本中,上面代碼中progress回調是不會進去的,應使用如下寫法:

一、promise給一個對象部署Deferred接口:

var dtd = $.Deferred(); // 新建一個deferred對象
var wait = function(dtd) {
    var i = 1,
        timer,
        percent; // 記錄進度
    var tasks = function() {
        if (i == 11) {
            alert("執行完畢!");
            dtd.resolve(); // 此操做完成後改變deferred對象的執行狀態
        } else {
            percent = (i * 500) / 5000 * 100 + '%';
            dtd.notify(percent); // 調用progress回調
            i++;
            setTimeout(tasks, 500);
        }
    };
    setTimeout(tasks, 1000);        
};
// 在wait對象上部署Deferred接口,此後就能夠直接在wait上使用deferred對象promise後的方法了
dtd.promise(wait);
// 在wait對象上使用deferred對象的方法指定回調。
wait.done(function() {
    alert("執行成功了!");
})
.fail(function() {
    alert("出錯啦!");
})
.progress(function(data) {
    console.log('執行中,已完成', data);
});
// 執行
wait(dtd);

二、使用$.Deferred傳入函數名:

var wait = function(dtd) {
    var i = 1,
        timer,
        percent; // 記錄進度

    var tasks = function() {
        if (i == 11) {
            alert("執行完畢!");
            dtd.resolve(); // 此操做完成後改變deferred對象的執行狀態
        } else {
            percent = (i * 500) / 5000 * 100 + '%';
            dtd.notify(percent); // 調用progress回調
            i++;
            setTimeout(tasks, 500);
        }
    };
    setTimeout(tasks, 1000);
    return dtd;
};    
// 綁定回調函數
$.Deferred(wait)
    .done(function() {
        alert("執行成功了!");
    })
    .fail(function() {
        alert("出錯啦!");
    })
    .progress(function(data) {
        console.log('執行中,已完成', data);
    });

deferred.promise()

promise方法的做用是在原來的deferred對象上返回另外一個deferred對象,後者只開放與改變執行狀態無關的方法(好比done()方法和fail()方法),屏蔽與改變執行狀態有關的方法(好比resolve()方法和reject()方法),從而使得執行狀態不能被改變。

  
        var dtd = $.Deferred(); // 新建一個Deferred對象
          
        var wait = function(dtd) {    
            var tasks = function() {      
                alert("執行完畢!");      
                dtd.resolve(); // 改變Deferred對象的執行狀態            
            };    
            setTimeout(tasks, 5000);    
            return dtd;  
        };  
        $.when(wait(dtd))  
            .done(function() {
                alert("執行成功!");
            })  
            .fail(function() {
                alert("出錯啦!");
            });
        dtd.reject(); // 改變狀態爲失敗,將當即觸發fail 5s後完成再出發done

若是咱們把deferred對象定義在了函數外部,那麼咱們設置deferred對象的狀態就會致使調用對應的回調。上面代碼中,最後調用reject方法,會致使當即調用了fail回調,5s以後又彈出執行完畢和執行成功。這將會致使沒必要要的混亂。使用promise方法就是一種解決方案。(以前寫的將var dtd = $.Deferred()放在函數內部,使得外部訪問不到也是一種解決方案)。

var dtd = $.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(); // d.resolve is not a function 通過promise後沒有resolve方法了

咱們看一下Deferred對象和它promise以後的區別。

promise返回的對象上已經去掉了和改變狀態有關的方法。notifynotifyWith是調用progress回調,resolvereject用於設置其狀態,帶with的方法可指定上下文環境。

此方法還能夠接收Object類型的參數,deferred.promise()會將事件綁定到該參數上,而後返回該對象,而不是建立一個新的對象。 這個方法能夠用於在已經存在的對象上綁定 Promise 行爲的狀況。示例以下:

var dtd = $.Deferred(); // 生成Deferred對象
  
var wait = function(dtd) {    
    var tasks = function() {      
        alert("執行完畢!");      
        dtd.resolve(); // 執行完畢後改變Deferred對象的執行狀態
            
    };    
    setTimeout(tasks, 5000);  
};   
// 在wait對象上部署Deferred接口,此後就能夠直接在wait上使用deferred對象promise後的方法了
dtd.promise(wait);
// 在wait對象上使用deferred對象的方法指定回調。
wait.done(function() {
    alert("哈哈,成功了!");
})  .fail(function() {
    alert("出錯啦!");
});  
// 執行
wait(dtd);

$.Deferred()

$.Deferred()除了建立一個deferred對象以外,能夠接受一個函數名(注意,是函數名)做爲參數,$.Deferred()所生成的deferred對象將做爲這個函數的默認參數。

var wait = function(dtd) {    
    var tasks = function() {      
        alert("執行完畢!"); 
        dtd.resolve();         
    };    
    setTimeout(tasks, 5000);      
};

$.Deferred(wait)
    .done(function() {
        alert("執行成功!");
    })
    .fail(function() {
        alert("執行失敗!");
    });

$.when()

接收一個或多個deferred對象做爲參數,爲其指定回調函數。

爲多個操做指定回調函數

deferred.then的傳遞做用

以前咱們介紹了,then方法接一到三個參數,分別指定deferred對象狀態爲已成功、已失敗和繼續等待的回調函數。

除此以外,then還能夠傳遞遲延對象。咱們知道,deferred能夠鏈式操做的緣由是其返回的還是deferred對象。then中的回調函數若是沒有返回新的deferred對象時,將依然使用最開始的那個deferred對象,若是在其也行返回一個deferred對象時,以後的操做將被轉移到在回調函數中return出來的新的deferred對象,從而進行傳遞。

咱們來看一個例子:

有5個js文件,其內容以下:

// perosn.js
var Person = function(name){
    this.name= name;
};

// person.prototype.js
if (Person) {
    Person.prototype.showName = function() {
        return this.name;
    };
} else {
    console.error('Person is undefined!');
}

// zs.js
var zs = new Person('張三');

// ls.js
var ls = new Person('李四');

// introduce.js
var introduceEachOther = function() {
    console.log('張三:你好,我叫' + zs.showName());
    console.log('李四:你好,我叫' + ls.showName());
};

文件內容都很是簡單,僅作演示使用,可是其中的依賴關係很是明顯,person.prototype.js依賴於perosn.js,zs.js和ls.js依賴於person.js,introduce.js依賴於其餘全部的js。

要分步加載,並保證可用,就必須保證在加載當前js時,其依賴的js已經加載完畢。

使用傳統的寫法,勢必會嵌套多層,不只邏輯很差處理,並且可讀性不好。

咱們用then演示一下如何來傳遞遞延對象來完成這個操做。

$.ajax({
    url: 'person.js',
    dataType: 'script'
}).then(function() {
    console.log(Person); // person.js已經加載成功
    console.log('person.js已經加載成功'); 
    return $.ajax({
        url: 'person.prototype.js',
        dataType: 'script'
    });
}).then(function(data) {
    // 這裏的data 上一步請求文件的內容 是person.prototype.js而非person.js
    console.log(data);
    console.log(Person.prototype.showName);
    console.log('person.prototype.js已經加載成功');
    // person.prototype.js 已經加載
    return $.when($.ajax({
        url: 'zs.js',
        dataType: 'script'
    }), $.ajax({
        url: 'ls.js',
        dataType: 'script'
    }));
}).then(function(){
    // zs.js 和 ls.js 都加載完成
    console.log(zs,ls);
    console.log('zs.js和zs.js已經加載成功');       
    return $.ajax({
        url: 'introduce.js',
        dataType: 'script'
    });
},function(){
    console.log('zs.js or ls.js failed');
}).then(function(){
    // 到此前面的全部資源都是加載成功的
    console.log('introduce.js已經加載成功,且其依賴資源都加載成功了,可使用了');       
    introduceEachOther();
});

在線地址

以上處理順序只是舉例演示,只要保證person.js最早加載,introduce.js最後加載,person.prototype.js在person.js以後便可。ls.js、zs.js與person.prototype.js的順序能夠調整。

咱們每次在then的第一個回調中返回了而一個新的遞延對象,以後的操做就是基於你返回的那個遞延對象了,第二個then回調中輸出的data證實了這一點。(若是沒有return出一個新的遞延對象的話,依然使用以前的那個。)

參考連接:

本文是照着下面的文章加上本身的學習體會書寫的。

阮一峯:jQuery的deferred對象詳解

相關文章
相關標籤/搜索