上回提到 TypeScript 2.1 讓 ES5 平台也能支援 async、await,形同 JavaScript 非同步程式的一場革命,衝著這點大家都該認真考慮改用 TypeScript。但 async、await 當真如此神奇?想想,上回漏講一個 await 殺手級應用案例,說服力有點弱,趕緊補上。

大家小時侯都有寫過這種需求:網頁進行更新、刪除操作前跳出對話框請使用者三思,回答「取消」可以反悔取消,回答「確定」才正式執行。在那個古老而純真的年代,只要寫一行就搞定:

if (confirm("下好離手,您確定要洗頭?")) { …倒洗髮精… }

等待 window.confirm() 傳回結果,依傳回值 true 或 false 決定後面流程,程式邏輯動線清楚明瞭。

但 confirm() 有幾個缺點,第一是畫面配置、文字、按鈕樣式由瀏覽器控器無法客製;第二點是等待使用者輸入的當下 JavaScript 執行緒將完全凍結,以 JavaScript 驅動的網頁元素互動或是 AJAX 連線都陷入失效狀態;第三,一旦呼叫 confirm 後我們就失去主導權,因此不可能實現「逾時未回應視為取消」,若使用者不回應,網頁只能地老天荒被卡著。

用個實例示範:

<!DOCTYPE html>
<html>
<body>
    <button>重新倒數</button>
    <div class="cnt-down"></div>
    <script src="Scripts/jquery-3.1.1.js"></script>
    <script>
        var countDown = 100;
        var hnd = setInterval(function () {
            if (countDown == 0) {
                alert("時間到!");
                clearInterval(hnd);
            }
            else 
                $(".cnt-down").text(countDown--);
        }, 1000);
        $("button").click(function () {
            if (confirm("確定要重新開始倒數?"))
                countDown = 100;
        });
    </script>
</body>
</html>

如以下展示,confirm 彈出「確定要重新開始倒數?」後,故意等幾秒才按取消,這段期間由 setInterval 驅動的倒數是停止的,直到按下取消才繼續。至於要做到「使用者五秒沒回應就取消 confirm」?黑洗謀摳零A代擠。

要克服上述缺點,就只能走上用 HTML 元素打造對話框(或使用 jQuery confirmKendo UI 等現成程式庫)的路。在非同步模式下想要依使用者按不同鈕執行不同邏輯,需仰賴 jQuery Deferred 或 Promise,並將確認及取消邏輯分別寫在 done()/fail() 或 then()/catch():(參考:使用自訂確認對話框取代window.confirm

<!DOCTYPE html>
<html>
<head>
    <title>Confirm Example</title>
    <meta charset="utf-8" />
</head>
<body>
    <button>重新倒數</button>
    <div class="cnt-down"></div>
 
    <div class='dialog' style='display:none'>
        <div class="my-cnfrm-diag" style='border: 1px solid blue; padding: 12px;'>
            <div class='m'></div><br />
            <input type='button' value='是' />
            <input type='button' value='否' />
        </div>
    </div>
 
    <script src="Scripts/jquery-3.1.1.js"></script>
    <script src="Scripts/jquery.blockUI.js"></script>
    <script>
        function myConfirm(msg) {
            var df = $.Deferred(); //建立Deferred物件
 
            //使用BlockUI顯示對話框
            $.blockUI({
                message: $(".dialog").html(),
                css: { width: "50%" }
            });
            
            //關閉對話框並傳回結果
            function close(result) {
                $.unblockUI(); //將對話框移除
                clearTimeout(hnd); //取消自動關閉排程
                if (result) df.resolve(); //使用者按下是
                else df.reject(); //使用者按下否
            }
 
            //若使用者未回應,五秒後自動關閉
            var hnd = setTimeout(function () {
                close(false);
            }, 5000);
 
            var $div = $(".my-cnfrm-diag");
            $div.find(".m").text(msg); //設定顯示訊息
 
            //加上按鈕事件
            $div.on("click", "input", function () {
                close(this.value == "是");
            });
 
            //傳回Promise
            return df.promise();
        }
    </script>
    <script>
        var countDown = 100;
        var hnd = setInterval(function () {
            if (countDown == 0) {
                alert("時間到!");
                clearInterval(hnd);
            }
            else
                $(".cnt-down").text(countDown--);
        }, 1000);
 
        $("button").click(function () {
            myConfirm("確定要重新開始倒數?")
                .done(function () {
                    countDown = 100;
                });
        });
    </script>
</body>
</html>

示範如下,顯示確認對話框的同時,數字仍會繼續倒數,第一次帶出對話框時,故意等五秒不操作,可以看到對話框被逾時機制自動關閉,直接認定取消;而第二次按下「是」時會觸發 jQuery Promise.done() 所設定的邏輯,將 countDown 重設回 100。

一切都符合需求,但確認後的執行動作必須寫在 myConfirm(…).done(function() { … }) 裡,不如過往直覺,要是能寫成 if (myConfirm(…)) { … } 就更完美了!

讓 await 登場實現我們的願望吧!

將程式搬進 TypeScript,TypeScript 是 JavaScript 的超集合,JavaScript 程式貼進 TypeScript 裡不用修改也能運作。myConfirm 部分先不動,先在最後一段動點手腳:

        function myConfirm(msg) {
            var df = $.Deferred(); //建立Deferred物件
 
            //使用BlockUI顯示對話框
            $.blockUI({
                message: $(".dialog").html(),
                css: { width: "50%" }
            });
            
            //關閉對話框並傳回結果
            function close(result) {
                $.unblockUI(); //將對話框移除
                clearTimeout(hnd); //取消自動關閉排程
                df.resolve(result); //傳回true/false
            }
 
            //若使用者未回應,五秒後自動關閉
            var hnd = setTimeout(function () {
                close(false);
            }, 5000);
 
            var $div = $(".my-cnfrm-diag");
            $div.find(".m").text(msg); //設定顯示訊息
 
            //加上按鈕事件
            $div.on("click", "input", function () {
                close(this.value == "是");
            });
 
            //傳回Promise
            return df.promise();
        }
 
var countDown = 100;
var hnd = setInterval(() => {
    if (countDown == 0) {
        alert("時間到!");
        clearInterval(hnd);
    }
    else
        $(".cnt-down").text(countDown--);
}, 1000);
 
$("button").click(async () => {
    if (await myConfirm("確定要重新開始倒數?")) {
        countDown = 100;
    }
});

button click 事件稍做修改,在匿名函式 () => { … } 前方加上 async 修飾,以便在其中使用 await。而 await myConfirm(…) 將等待 Promise Resolve 或 Reject 才繼續執行,讓邏輯回歸 if (confirm(…)) { … } 般的單純直覺。

還有一個地方要小調,await myConfirm() 傳回結果將等於 Resolve() 傳回值,若遇上 Reject() 會得到 undefined 並產生 "Uncaught (in promise)" 錯誤,故 myConfirm 裡原本 if (result) df.resolve() else df.reject() 寫法改為一律 Resolve(),再依傳回值 true/false 區分使用者按是或按否。小事一椿,改成 df.resolve(result) 就搞定。

從單純的 if (confirm(…)) { … } 演進好用但寫法不直覺的 myConfirm(…).done(function() { … }),回歸 if (await myConfirm(…)) { … } 的清楚流程 ,大師兄回來了,謝謝 TypeScript await!


Comments

Be the first to post a comment

Post a comment