Deferred 和 Promise
ES6 和 jQuery 都有 Deffered 和 Promise,但是略有不同。不過它們的作用可以簡單的用兩句話來描述
- Deffered 觸發 resolve 或 reject
- Promise 中申明 resolve 或 reject 後應該做什麼(回撥)
在 jQuery 中
var deferred = $.Deferred();
var promise = deferred.promise();複製程式碼
在 ES6 中
var deferred = Promise.defer();
var promise= defered.promise;複製程式碼
MDN 宣佈 Deferred 在 Gecko 30 中被申明為過期,不應該再使用,而應該用
new Promise()
來代替。關於new Promise()
將在後面說明。
jQuery 的 Deferred/Promise
jQuery 中最常用的 Promise 物件是 $.ajax()
返回的,最常用的方法不是 then
,而是 done
、fail
和 always
。除了 $.ajax()
外,jQuery 也提供了 $.get()
、$.post()
和 $.getJSON()
等簡化 Ajax 呼叫,它們返回的和 $.ajax()
的返回值一樣,是個 Promise 物件。
實際上
$.ajax()
返回的是一個 jqXHR 物件。但 jqXHR 實現了 jQuery 的 Promise 介面,所以也是一個 Promise 物件。
done()
、fail()
和 always()
done()
新增 deferred.resolve()
的回撥,fail()
新增 deferred.reject()
的回撥。所以在 Ajax 呼叫成功的情況下執行 done()
新增的回撥,呼叫失敗時執行 fail()
新增的回撥。但不管成功與否,都會執行 always()
新增的回撥。
這裡 done()
、fail()
和 always()
都是以類似事件的方式新增回撥,也就意味著,不管執行多次次 done()
、fail()
或 always()
,它們新增的若干回撥都會在符合的條件下依次執行。
一般情況下會這樣執行 Ajax
// 禁用按鈕以避免重複提交
$("#theButton").prop({
disabled: true
});
// 呼叫 Ajax 提交資料,假設返回的是 JSON 資料
var jqxhr = $.ajax("do/example", {
type: "post",
dataType: "json",
data: getFormData()
});
jqxhr.done(function(jsonObject) {
// Ajax 呼叫成功
console.log("success with data", jsonObject);
}).fail(function() {
// Ajax 呼叫失敗
console.log("failed")
}).always(function() {
// 不管成功與否,都會執行,取消按鈕的禁用狀態
$("#theButton").prop({
disabled: false
});
});複製程式碼
上面是最普通最常用的用法,但是在一個專案中總是這麼寫 Ajax,有點累,稍微約定一下再封裝一下就使用起來就會便捷得多。首先,假設我們定義返回的 JSON 是這樣的格式:
{
"code": "int, 0 表示成功,其它值表示出錯",
"message": "string, 附加的訊息,可選",
"data": "object,附加的資料,可選
}複製程式碼
然後為專案公共類 app
定義一個 ajax
方法
app.ajax = function(button, url, data) {
if (button) {
button.prop("disabled", true);
}
return $.ajax(url, {
type: "post",
dataType: "json",
data: data
}).done(function(json) [
if (json.code !== 0) {
showError(json.message || "操作發生錯誤");
}
}).fail(function() {
showError("伺服器錯誤,請稍後再試");
}).always(function() {
if (button) {
button.prop("disabled", false);
}
});
};
// 呼叫
app.ajax("do/example", getFormData()).done(function(json) {
if (json.code === 0) {
// 只需要處理正確的情況啦
}
});複製程式碼
不過還是有點不爽,如果不需要判斷 json.code === 0
就更好了。這個……可以自己用一個 Deferred 來處理:
app.ajax = function(button, url, data) {
if (button) {
button.prop("disabled", true);
}
var deferred = $.Deferred();
$.ajax(url, {
type: "post",
dataType: "json",
data: data
}).done(function(json) [
if (json.code !== 0) {
showError(json.message || "操作發生錯誤");
deferred.reject();
} else {
deferred.resolve(json);
}
}).fail(function() {
showError("伺服器錯誤,請稍後再試");
deferred.reject();
}).always(function() {
if (button) {
button.prop("disabled", false);
}
});
return deferred.promise();
};
// 呼叫
app.ajax("do/example", getFormData()).done(function(json) {
// json.code === 0 總是成立
// 正常處理 json.data 就好
});複製程式碼
注意,這裡已經不是直接返回 $.ajax()
的結果 jqXHR 物件了,返回的是新建 Deferred
物件的 promise
物件。
複習了 Ajax,現在需要切入正題,找到 jQuery Promise 和 ES6 Promise 接近的地方——then()
。
jQuery deferred.then()
在 jQuery 1.8 以前(不含 1.8,比如 jQuery 1.7.2),deferred.then()
就是一個把 done()
和 fail()
放在一起的語法糖。jQuery 在 1.8 版本的時候修改了 deferred.then()
的行為,使 then()
的行為與 Promise 的 then()
相似。從 jQuery 的文件可以看到 1.8 版本的變化——幹掉了 callback,換成了 filter:
// version added: 1.5, removed: 1.8
deferred.then( doneCallbacks, failCallbacks )
// version added: 1.7, removed: 1.8
deferred.then( doneCallbacks, failCallbacks [, progressCallbacks ] )
// version added: 1.8
deferred.then( doneFilter [, failFilter ] [, progressFilter ] )複製程式碼
可以簡單的把 callback 當作一個事件處理,值用於 callback 之後一般不會改變;而 filter 不同,一個值傳入 filter 再從 filter 返回出來,可能已經變了。還是舉個例子來說明
var deferred = $.Deferred();
var promise = deferred.promise();
promise.then(function(v) {
console.log(`then with ${v}`);
}).done(function(v) {
console.log(`done with ${v}`);
});
deferred.resolve("resolveData");複製程式碼
在 jQuery 1.7.2 中的結果
then with resolveData
done with resolveData複製程式碼
在 jQuery 1.8.0 中的結果
then with resolveData
done with undefined複製程式碼
從上面來看,jQuery 的 deferred.then()
語義和 ES6 Promise.then()
語義基本一致。如果把上面的 app.ajax
換成 then()
實現會有助於對 ES6 Promise 的理解。
app.ajax = function(button, url, data) {
if (button) {
button.prop("disabled", true);
}
return $.ajax(url, {
type: "post",
dataType: "json",
data: data
}).then(function(json) {
if (json.code !== 0) {
showError(json.message || "操作發生錯誤");
return $.Deferred().reject().promise();
} else {
return $.Deferred().resolve(json).promise();
}
}, function() {
showError("伺服器錯誤,請稍後再試");
deferred.reject();
}).always(function() {
if (button) {
button.prop("disabled", false);
}
});
};
// 呼叫方式沒變,用 done,也可以用 then
app.ajax("do/example", getFormData()).done(function(json) {
// json.code === 0 總是成立
// 正常處理 json.data 就好
});複製程式碼
從 jQuery Promise 到 ES6 Promise
上面的程式碼太長,提煉一下關鍵部分(示意,不能執行)
var promise = $.ajax();
promise.then(function(data) {
// resolve
return data.code
? new Promise().reject()
: new Promise().resolve(data);
// 如果沒有錯,就返回一個新的 promise,並使用 data 來 resolve,
// 也可以直接返回 data,
// 這樣後面 then 的 resolve 部分才能收到資料
}, function() {
// rejected
});
// 呼叫階段
promise.then(function(data) {
// 處理 data
});複製程式碼
也許你沒注意到,其實上面的程式碼基本上就是 ES6 的 Promise 了。下面正式用 ES6 Promise 改寫上面的示意程式碼
var promise = new Promise(function(resolve, reject) {
$.ajax().then(resolve, reject);
// 上面這句沒看懂?那換成這樣你一定會懂
// $.ajax().then(function(data) {
// resolve(data);
// }, function() {
// reject();
// });
}).then(function(data) {
return data.code
? Promise.reject()
: Promise.resolve(data);
// 這裡 Promise.resolve(data) 同樣可以直接替換為 data
});
// 呼叫沒變
promise.then(function(data) {
// 處理 data
});複製程式碼
怎麼樣,差別不大吧。不知不覺就會 ES6 Promise 了!
ES6 的 Promise
上面已經把 ES6 的 Promise 帶出來了,現在只需要把常用方法列出來作為參考即可
注意,小寫的
promise
表示Promise
物件
-
new Promise(executor)
,產生一個新的 Promise 物件executor(resolve, reject)
executor
、resolve
和reject
均為函式,在executor
中,正確處理呼叫resolve()
返回資料,異常處理直接throw new Error(...)
或調reject()
返回資料。 -
Promise.resolve(data)
,產生 Promise 物件並resolve
-
Promise.reject()
,產生 Promise 物件並reject
-
promise.then(onResolve, onReject)
,然後……繼續處理 -
promise.catch(onReject)
,project.then(null, onReject)
的語法糖,和 jQuery 的promise.fail()
差不多(但不同)。
參考
- ECMAScript 2015 Language Specification – ECMA-262 6th Edition
- Deferred – Mozilla | MDN
- Promise – Mozilla | MDN
- Deferred Object | jQuery Documentation
本文2015年8月首於發 SegmentFault,最近整理再發