隨著野生 IE 消失殆盡,我的前端開發進入新時代,不必再依賴 jQuery 幫忙跨瀏覽器,還可安心使用各式新式 JavaScript 及 CSS 語法。現在若不是要用 jQuery 套件,簡單程式我多半會用香草 JavaScript (Vanilla JavaScript)輕鬆搞定。涉及複雜輸入欄位互動或套版顯示,再搬出 Vue.js 實作 MVVM從容應戰,寫起輕前端網頁比過去簡單許多。

非同步作業在 jQuery 時代是靠 Deferred,在原生 JavaScript 則是用 Promise,存取網路資源或呼叫 API 使用的 fetch() 方法,傳回型別也正是 Promise。換言之,Promise 已是現在寫 JavaScript 不可不知的基本知識了。

很不幸,即便是現在,每每遇上複雜一點 Promise 情境,像是同時做 A、B、C,都做完時再做 D,或是接續執行 A、B、C,前面成功才執行下一個... 我的頭腦還是會卡頓一下,要仔細想想或查範例才寫得出來。這一定是還沒寫筆記的關係,我想,所以就有了這篇。

我整理了三種典型的 Promise 應用情境,寫成範例當成日後腦霧時參考:

  1. Promise 串接:A 成功再做 B
  2. 平行執行:同時執行 A、B、C,並等待它們完成
  3. 循序執行:串接不定數量的 A、B、C... 作業,前一個成功再執行下一個

Promise 的起手式如下:(補充:MDN 關於 Promise 的介紹)

// 建立 Promise 物件,傳入接收 resolve, reject 方法的自訂函式
const promise = new Promise((resolve, reject) => {
   // 進行非同步作業
   // 執行完畢,若成功呼叫 resolve(),會觸發 .then() 
   // resolve(data) 可將 data 傳給 then()
   // 失敗時呼叫 reject(),觸發 .catch()
   // reject(err) 可將 err 傳給 catch()
});

// 等待執行結果後要執行的邏輯
promise.then((data) => ...) // 執行成功要做的動作
    .catch((err) => ...) // 失敗時要做的動作

為方便展示,我寫了一個小函式模擬非同步化作業,它會在頁面顯示 OK、Cancel 兩顆鈕,讓操作者模擬成功( resolve() 觸發 then() )或失敗( reject() 觸發 catch() )兩種情境。

function waitForAck(tile) {
    // 傳回 Promise
    return new Promise((resolve, reject) => {
        const fieldset = document.createElement('fieldset');
        fieldset.innerHTML = `<legend>${tile}</legend>
        <button>OK</button>
        <button>Cancel</button>`;
        fieldset.addEventListener('click', (e) => {
            // fieldset click 事件中檢查是否按下 OK 或 Cancel 鈕
            if (e.target.tagName === 'BUTTON') {
                if (e.target.textContent === 'Cancel')
                    reject(); // Reject
                else {
                    log(`${tile} 完成`);
                    resolve(); // Resolve
                }
                fieldset.remove(); // Reject 或 Resolve 後移除按鈕
            }
        });
        document.querySelector('#buttons').appendChild(fieldset);
    });
}

先附上範例程式及線上展示

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Promise 循序與同步執行</title>
    <style>
        html,
        body,
        button {
            font-family: Arial, sans-serif;
            font-size: 9pt;
        }

        #buttons {
            display: flex;
            min-height: 60px;
            margin: 6px 0;
            background-color: #eee;
            padding: 12px;
        }
    </style>
</head>

<body>
    <div>
        <button onclick="testConcat()">Promise 串接</button>
        <button onclick="testParallel()">平行執行</button>
        <button onclick="testSerial()">循序執行</button>
    </div>
    <div id="buttons"></div>
    <ul id="logs"></ul>
    <script>
        function log(msg) { // 顯示 log
            const p = document.createElement('li');
            p.textContent = msg;
            document.querySelector('#logs').appendChild(p);
        }
        function clear() { // 清除 log
            document.querySelector('#logs').innerHTML = '';
        }
        function waitForAck(tile) {
            return new Promise((resolve, reject) => {
                const fieldset = document.createElement('fieldset');
                fieldset.innerHTML = `<legend>${tile}</legend>
                <button>OK</button>
                <button>Cancel</button>`;
                fieldset.addEventListener('click', (e) => {
                    if (e.target.tagName === 'BUTTON') {
                        if (e.target.textContent === 'Cancel')
                            reject();
                        else {
                            log(`${tile} 完成`);
                            resolve();
                        }
                        fieldset.remove();
                    }
                });
                document.querySelector('#buttons').appendChild(fieldset);
            });
        }
        function testConcat() {
            clear();
            log('Promise 串接');
            waitForAck('Promise A')
                .then(() => waitForAck('Promise B'))
                .then(() => log('執行完畢'))
                .catch(() => log('未完成'));
        }
        const codes = ['A', 'B', 'C'];
        function testParallel() {
            clear();
            log('平行執行');
            const jobs = [];
            for (const c of codes) {
                jobs.push(waitForAck(`Promise ${c}`));
            }
            Promise.all(jobs)
                .then(() => log('執行完畢'))
                .catch(() => log('未完成'));
        }
        function testSerial() {
            clear();
            log('循序執行');
            const jobs = [];
            for (const c of codes) {
                // 不要直接呼叫 waitForAck,而是將呼叫 waitForAck 的函式推入 jobs 陣列
                jobs.push(() => waitForAck(`Promise ${c}`));
            }
            // 這裡的 reduce() 相當於
            // let p = Promise.resolve();
            // for (const job of jobs) p = p.then(() => job());
            jobs.reduce((p, job) => p.then(() => job()), Promise.resolve())
                .then(() => log('執行完畢'))
                .catch(() => log('未完成'));
        }

    </script>
</body>

</html>

再來看看實測結果:

  1. Promise 串接
    A resolve() 才會執行 B,A、B 都 resolve() 最後的 then() 才會顯示「執行完畢」

  2. 平行執行
    同時顯示三個 Promise 的操作介面,三個都按 OK 才會出現「執行完畢」

  3. 循序執行
    A 成功才執行 B、B 成功執行 C,三個都成功才會得到「執行完畢」

This post provides three Promise examples of Vanilla JavaScript: chaining, parallel execution, and sequential execution.


Comments

Be the first to post a comment

Post a comment