維護古蹟過程遇到要串接多個非同步作業的需求,例如:Warn()、UpdateConfirm() 與 CallApi() 為非同步作業(詢問使用者確認或取消、呼叫 WebAPI),函式有點年紀故傳回的是 jQuery Promise,我要做到 Warn() 確定才呼叫 UpdateConfirm(),UpdateConfirm() 才呼叫 CallApi()。這不是什麼新鮮需求,翻到 11 年前寫的文章 - 以 jQuery 循序執行 AJAX 呼叫,並依結果決定是否繼續 準備抄 Code,卻發現它過期了。文章用的 jQuery deferred.pipe() 方法,早在 jQuery 1.8 版就宣告棄用,建議改用 deferred.then()

為了與時俱進,今天來補上新寫法。以下是個簡單範例,假設有 step1 到 step3 三個非同步方法,它們都會傳回 deferred.promise(),方便後續串 then()、done()、fail();非同步操作我借用 SweetAlert2 彈出對話框詢問繼續或中止,三次詢問都答 OK 才算完成,任一次按 STOP 就中止。

另外,我再增加了 step1() 傳資料給 step2()、step2() 傳資料給 step3() 的橋段,以貼近實務上可能出現的需求。主要串接寫法如下,還算好懂,step1().then((r) => step2(r)) 代表 step1() 若成功(其中呼叫 deferred.resolve()) 才執行 step2(),而 r 可承接其 deferred.resolve(x) 傳入的 x。

log('START');
step1()
    .then((r) => step2(r))
    .then((r) => step3(r))
    .then(() => log('END'))
    .fail(() => log('CANCEL'));

完整程式碼如下,並附上線上展示

<!DOCTYPE html>
<html>
<head>
    <title>jQuery Chained Promise Demo</title>
</head>
<body>
    <div id="msgs">
    </div>
    <script src="https://unpkg.com/jquery"></script>
    <script src="https://cdn.jsdelivr.net/npm/sweetalert2"></script>
    <script>
        function showConfirm(title, msg) {
            return new Promise((resolve, reject) => {
                Swal.fire({
                    title: title,
                    text: msg,
                    icon: 'info',
                    showCancelButton: true,
                    confirmButtonText: 'OK',
                    cancelButtonText: 'STOP'
                }).then((result) => {
                    if (result.value) {
                        resolve();
                    } else {
                        reject();
                    }
                });
            });
        }
        let log = (msg) => {
            $("#msgs").append("<div>" + msg + "</div>");
        }
        var step1 = () => {
            const dfd = jQuery.Deferred();
            log('STEP 1');
            showConfirm('STEP1', 'Go?')
                .then(() => dfd.resolve('R1'))
                .catch(() => dfd.reject());
            return dfd.promise();
        };
        var step2 = (res) => {
            const dfd = jQuery.Deferred();
            log(`STEP 2 (${res})`);
            showConfirm('STEP2', 'Go?')
                .then(() => dfd.resolve('R2'))
                .catch(() => dfd.reject());
            return dfd.promise();
        };
        var step3 = (res) => {
            const dfd = jQuery.Deferred();
            log(`STEP 3 (${res})`);
            showConfirm('STEP3', 'Go?')
                .then(() => dfd.resolve())
                .catch(() => dfd.reject());
            return dfd.promise();
        };
        log('START');
        step1()
            .then((r) => step2(r))
            .then((r) => step3(r))
            .then(() => log('END'))
            .fail(() => log('CANCEL'));
    </script>
</body>
</html>

講到與時俱進,如果沒有舊系統的包袱,其實這個需求用純 JavaScript 就可以解決,不需動用 jQuery,所以研究一下香草 JS 怎麼寫也是必要的。而基本語法改寫這類小事,2023 起已是 AI 的主場,搞懂原理就好(之前有研究過 JavaScript Promise)不需親力親為,省下寶貴的腦力體力做更重要的事。

讓我們來呼叫會寫程式的阿拉丁神燈 - Github Copilot

在 VSCode 選取整段 jQuery 程式碼,滑鼠右鍵選單 Copilot / Start Code Chat 或按快捷鍵 Ctrl+I:

許願 rewrite the code, replace jQuery.Deferred with Promise (敲中文也行,打英文比較快,而 AI 十分聰明友善,能無視錯字漏寫文法錯誤,隨便寫它也看得懂)

送出後稍等幾秒鐘,Github Copilot 已將整段改寫好,看看若沒錯誤(這個步驟不能省,AI 產生結果還是要自己看過再放進作品,不然哪天怎麼死的都不知道),按下方的 Accept 接收鈕可將程式碼置換成 Copilot 改寫的版本。

或者你還可以繼續下指令請 Copilot 優化程式碼,像是 extract shared function from step1, step2, setp3 請它將 step1 ~ step3 相似部分抽取成共用函式。許願成功,Coplit 將重複程式碼提取成 confirmStep 函式。

Copilot 改寫後版本如下,這類簡單 JavaScript 程式若無意外,一般不用修改便能執行。線上展示

<!DOCTYPE html>
<html>
<head>
    <title>jQuery Chained Promise Demo</title>
</head>
<body>
    <div id="msgs">
    </div>
    <script src="https://cdn.jsdelivr.net/npm/sweetalert2"></script>
    <script>
        function showConfirm(title, msg) {
            return new Promise((resolve, reject) => {
                Swal.fire({
                    title: title,
                    text: msg,
                    icon: 'info',
                    showCancelButton: true,
                    confirmButtonText: 'OK',
                    cancelButtonText: 'STOP'
                }).then((result) => {
                    if (result.value) {
                        resolve();
                    } else {
                        reject();
                    }
                });
            });
        }
        function log(msg) {
            const msgs = document.querySelector("#msgs");
            const div = document.createElement("div");
            div.textContent = msg;
            msgs.appendChild(div);
        }

        function confirmStep(step, message) {
            return new Promise((resolve, reject) => {
                log(`${step}`);
                showConfirm(step, message)
                    .then(() => resolve())
                    .catch(() => reject());
            });
        }

        var step1 = () => {
            return new Promise((resolve, reject) => {
                confirmStep('STEP 1', 'Go?')
                    .then(() => resolve('R1'))
                    .catch(() => reject());
            });
        };

        var step2 = (res) => {
            return new Promise((resolve, reject) => {
                confirmStep(`STEP 2 (${res})`, 'Go?')
                    .then(() => resolve('R2'))
                    .catch(() => reject());
            });
        };

        var step3 = (res) => {
            return new Promise((resolve, reject) => {
                confirmStep(`STEP 3 (${res})`, 'Go?')
                    .then(() => resolve())
                    .catch(() => reject());
            });
        };

        log('START');
        step1()
            .then((r) => step2(r))
            .then((r) => step3(r))
            .then(() => log('END'))
            .catch(() => log('CANCEL'));
    </script>
</body>
</html>

2023 年,寫了幾十年的程式老人正在學習適應 AI 新時代。

Example of how to chain serveral async functions which return jQuery prmises and run then one by one.


Comments

# by 小黑

nice

# by Wen

與時俱進也是很不容易的能力

Post a comment