非同步邏輯是寫 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="/img/loading.svg" data-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 的整合應用可以通行無阻,火力升級。


Comments

# by 凱大

開源最大的好處就是 有問題可以自己修 關鍵字重複而且又很想用的話 把source code 的function name 換掉 再用TypeScript 寫interface 變回來就好了 ~~ 在老舊應該也可以follow 這是TypeScript Magic XD

Post a comment