非同步邏輯是寫 JavaScript 逃不掉的複雜課題,古早流行的做法是傳入 Callback 函式當參數,待特定作業完成再呼叫,缺點是串接程序一旦變多,就會出現波動拳式排版,寫到渾然不知身處夢境第幾層:
asyncJob1(function() {
//Callback 函式: asyncJob1 完成後呼叫
//......
ayncJob2(function() {
//Callback 函式: asyncJob2 完成後呼叫
//......
ayncJob3(function() {
//Callback 函式: asyncJob3 完成後呼叫
//......
ayncJob4(function() {
//Callback 函式: asyncJob4 完成後呼叫
//......
ayncJob5(function() {
//Callback 函式: asyncJob5 完成後呼叫
//......
});
});
});
});
});
後來,使用 Promise 串連非同步邏輯漸成主流,作業成功或失敗 Callback 寫在 Promise 物件 done()/then()/fail() 等方法,另外還有 always() 指定不論成功失敗都要執行的程序。各大程式庫與框架(jQuery 1.5+、Angular)都有自己實做的 Promise 版本,原理大同小異,要串接循序執行的連續作業是小事一椿,就以 jQuery 為例,上述程式碼可以改寫美化如下:(註:1.8 起建議以 then() 取代 pipe())
var dfd = jQuery.Deferred();
dfd.resolve()
.then(function() {
return asyncJob1();
})
.then(function() {
//Callback 函式: asyncJob1 完成後呼叫
//......
return asyncJob2();
})
.then(function() {
//Callback 函式: asyncJob2 完成後呼叫
//......
return asyncJob3();
})
.then(function() {
//Callback 函式: asyncJob3 完成後呼叫
//......
return asyncJob4();
})
.then(function() {
//Callback 函式: asyncJob4 完成後呼叫
//......
return asyncJob5();
})
.done(function() {
//Callback 函式: asyncJob5 完成後呼叫
//......
});
理論上 jQuery 或 Angular 實做的 Promise 已經很夠用,但自從 Promise被納入 ECMAScript 6 規範,意味著新一代瀏覽器都內建支援,不再需要第三方程式庫,未來使用標準 Promise 處理非同步邏輯將成主流,又有新東西要學了。
ES6 的 Promise 依循 Promise/A+規範,寫法跟我慣用的 jQuery Deferred 有點出入,搞個對照,熟悉 Promise 寫法是必要的。(註:如果你被 ECMAScript 6、ES6、ES2015 等術語搞到頭很昏,可以參考這篇)
程式操作以上,程式用 Promise 處理非同步流程,按下 Resolve 或 Reject 彈出不同訊息並停用兩顆按鈕。如果用 jQuery + TypeScript,寫法如下:
module test1 {
var dfd = jQuery.Deferred();
$("#btnResolve").click(() => dfd.resolve());
$("#btnReject").click(() => dfd.reject());
var task = dfd.promise();
task.done(
() => {
alert("Button Resolve Clicked!");
})
.fail(
() => {
alert("Button Reject Clicked!");
})
.always(() => {
$("button").prop("disabled", true);
});
}
建立一個 jQuery.Deferred,呼叫 promise() 產生 Promise 物件,在 done()、fail()、always() 分別掛上事件,呼叫 resolve() 將觸發 done()、呼叫 reject() 則會觸發 fail(),而不管 resolve() 或 reject() 最後都會觸發 always()。
來看看改成 ES6 Promise 要怎麼寫:
module test2 {
declare var Promise: any;
var task = new Promise((resolve, reject) => {
$("#btnResolve").click(() => resolve());
$("#btnReject").click(() => reject());
});
task.then(
() => {
alert("Button Resolve Clicked!");
})
.catch(
() => {
alert("Button Reject Clicked!");
})
.then(
() => {
$("button").prop("disabled", true);
});
}
原理大同小異,主要差別在 resolve、reject 在 Promise 建構時傳入,而 resolve() 觸發 then()、reject() 觸發 catch(),要實現 jQuery Deferred always() 則可在 catch() 後方再加一個 then()。
開啟 Chrome、Edge 或 Firefox,可以觀察到 ES6 Promise 版網頁的操作效果跟 jQuery Deferred 版完全相同。等等,那 IE …
是的,即便是 IE11 也沒內建 Promise!該怎麼辦?
網路上有不少 Promise Polyfill,例如:ES6 Promise、BludbirdJS,Promise 的原理不複雜,自己寫一個也非不可能,MSDN Blog 甚至有一篇範例,但應該很少人會想為此造輪子。研究後,我找到引用 Lightweight ES6 Promise polyfill的簡單做法,下載 Github 上的 promise.js 或 promise.min.js,網頁加上 <script src="Scripts/promise.js"></script>就一切搞定。如果你身為要考慮 IE678 的悲情攻城獅(老師,金包銀前奏請下),又偏偏不肯向命運低頭誓死要挑戰在老 IE 用 Promise,由於 catch 對老 IE 是保留字,.catch(…) 得改寫成 ["catch"](…) 才不會出錯。(話說回來,跨瀏覽器又要考慮 IE,放著 jQuery 不用硬要橫柴入灶是為哪椿呢?)
最後補充一點,jQuery 3 調整了 jQuery.Deferred 以符合 Promise/A+,若 jQuery 已升級到 3.0,Deferred 可以當成標準 Promise 使用,遇到要求型別為 Promise 的整合應用可以通行無阻,火力升級。