上回提到 TypeScript 2.1 讓 ES5 平台也能支援 async、await,形同 JavaScript 非同步程式的一場革命,衝著這點大家都該認真考慮改用 TypeScript。但 async、await 當真如此神奇?想想,上回漏講一個 await 殺手級應用案例,說服力有點弱,趕緊補上。
大家小時侯都有寫過這種需求:網頁進行更新、刪除操作前跳出對話框請使用者三思,回答「取消」可以反悔取消,回答「確定」才正式執行。在那個古老而純真的年代,只要寫一行就搞定:
if (confirm("下好離手,您確定要洗頭?")) { …倒洗髮精… }
等待 window.confirm() 傳回結果,依傳回值 true 或 false 決定後面流程,程式邏輯動線清楚明瞭。
但 confirm() 有幾個缺點,第一是畫面配置、文字、按鈕樣式由瀏覽器控器無法客製;第二點是等待使用者輸入的當下 JavaScript 執行緒將完全凍結,以 JavaScript 驅動的網頁元素互動或是 AJAX 連線都陷入失效狀態;第三,一旦呼叫 confirm 後我們就失去主導權,因此不可能實現「逾時未回應視為取消」,若使用者不回應,網頁只能地老天荒被卡著。
用個實例示範:
<!DOCTYPEhtml>
<html>
<body>
<button>重新倒數</button>
<divclass="cnt-down"></div>
<scriptsrc="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 confirm、Kendo UI等現成程式庫)的路。在非同步模式下想要依使用者按不同鈕執行不同邏輯,需仰賴 jQuery Deferred 或 Promise,並將確認及取消邏輯分別寫在 done()/fail() 或 then()/catch():(參考:使用自訂確認對話框取代window.confirm)
<!DOCTYPEhtml>
<html>
<head>
<title>Confirm Example</title>
<metacharset="utf-8"/>
</head>
<body>
<button>重新倒數</button>
<divclass="cnt-down"></div>
<divclass='dialog'style='display:none'>
<divclass="my-cnfrm-diag"style='border: 1px solid blue; padding: 12px;'>
<divclass='m'></div><br/>
<inputtype='button'value='是'/>
<inputtype='button'value='否'/>
</div>
</div>
<scriptsrc="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!