jQuery Deferred / Promise

現今的網頁開發越來越複雜了,同時也有更多異步 (asynchronous) 的操作 (像是 Ajax),對於異步,通常的做法是用 callback,當事情完成後調用這些 callback 執行後續動作。但 callback 太多層或同時要等待多個異步事件時,會讓程式碼很亂難以管理也容易出錯 (callback hell)。

等一件事執行完畢後,接續執行的函式就是所謂的 callback 函式。

callback 太多層?像這樣...

step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // Do something with value4
      });
    });
  });
});

jQuery 提供了 Deferred 來更好的處理這些異步問題,defer 字面上的意思就是延遲,而 deferred object 就是延遲到未來某個時間點再執行的物件。

建立 Deferred 物件 - jQuery.Deferred()

var dfd = $.Deferred();

deferred.done(), deferred.fail(), deferred.always()

當 deferred object 處理完成且成功時,會執行透過 deferred.done() 註冊的 callback;當 defer object 處理失敗時,會執行透過 deferred.fail() 註冊的 callback;而不管成功或失敗,都會執行透過 deferred.always() 註冊的 callback。

var dfd = $.Deferred();

dfd.done(function() {
    alert('成功了');
}).fail(function() { // 串接
    alert('失敗了');
});

// 隨時可以用 deferred object 註冊新的 callback
dfd.always(function() {
    alert('不管成功或失敗');
});

deferred.resolve(), deferred.reject()

但怎麼完成一個 deferred object,然後執行對應的 callback functions?

deferred 物件有三種執行狀態 - 未完成、已完成、已失敗。我們可以用 resolve 來改變執行狀態為成功;用 reject 來改變執行狀態為失敗。

deferred.resolve() 結束 deferred object 的執行狀態 (成功),並執行 doneCallbacks, alwaysCallbacks。

deferred.reject() 結束 deferred object 的執行狀態 (失敗),並執行 failCallbacks, alwaysCallbacks。

狀態只能改變一次,不能又 resolve 又 reject。
var dfd = $.Deferred();
 
dfd.done(function() {
    alert('你點了成功按鈕');
});

dfd.fail(function() {
    alert('你點了失敗按鈕');
});
 
$('button.success').on('click', function() {
  // 通知成功
  dfd.resolve();
});

$('button.fail').on('click', function() {
  // 通知失敗
  dfd.reject();
});

resolve() 和 reject() 方法還可以接受一個參數,用來傳入 callback function。

var dfd = $.Deferred();
 
dfd.done(function(name) {
    alert('Your name is ' + name);
});
 
$('button').on('click', function() {
  dfd.resolve('Mike');
});

deferred.then(doneCallbacks, failCallbacks)

有時為了省事,我們可以用 .then() 來將 .done() 和 .fail() 合在一起寫。

dfd.then(
  function() {
    alert('succeeded');
  }, function() {
    alert('failed!');
  }
);

deferred.state()

用 deferred.state() 來取得目前的執行狀態,有三種返回值:

  • "pending": 未完成
  • "resolved" : 已完成
  • "rejected": 已失敗

jQuery.when(deferreds)

.when() 讓你可以為多個 Deferred 事件指定一個 callback,等所有的異步事件都結束後,再執行這個 callback。

var d1 = $.Deferred();
var d2 = $.Deferred();
var d3 = $.Deferred();
 
$.when(d1, d2, d3).done(function (v1, v2, v3) {
  console.log(v1); // v1 is undefined
  console.log(v2); // v2 is "abc"
  console.log(v3); // v3 is an array [1, 2, 3, 4, 5]
});
 
d1.resolve();
d2.resolve('abc');
d3.resolve(1, 2, 3, 4, 5);

deferred.promise()

Promise 和 Deferred 是很類似的東西,除了 Promise object 少了改變狀態的方法 - .resolve(), .reject()。好處是什麼?如果你有 function 需要返回 deferred object,但你又不想讓其他的程式亂改狀態,你可以改成返回 promise!

function asyncEvent() {
  var dfd = jQuery.Deferred();
 
  // 亂數幾秒後 resolve 狀態
  setTimeout(function() {
    dfd.resolve('hurray');
  }, Math.floor(400 + Math.random() * 2000));
 
  // 亂數幾秒後 reject 狀態
  setTimeout(function() {
    dfd.reject('sorry');
  }, Math.floor(400 + Math.random() * 2000));
 
  // 返回一個 promise 避免被亂搞狀態
  return dfd.promise();
}
 
// Attach a done, fail, and progress handler for the asyncEvent
$.when( asyncEvent() ).then(
  function(status) {
    alert(status + ', things are going well');
  },
  function( status ) {
    alert(status + ', you fail this time');
  }
);