25. 承諾用於非同步程式設計
目錄
請支持這本書:購買它(PDF、EPUB、MOBI)捐款
(廣告,請不要封鎖。)

25. 承諾用於非同步程式設計

本章節是關於非同步程式設計的介紹,特別是透過 Promises 和 ECMAScript 6 Promise API。 上一章說明了 JavaScript 中非同步程式設計的基礎。當您在本章中遇到任何不理解的地方時,可以參閱它。



25.1 概觀

Promise 是提供非同步運算結果的另一種選擇,取代回呼。非同步函數的實作者需要付出更多努力,但這些函數的使用者可以獲得許多好處。

下列函數透過 Promise 非同步傳回結果

function asyncFunc() {
    return new Promise(
        function (resolve, reject) {
            ···
            resolve(result);
            ···
            reject(error);
        });
}

您呼叫 asyncFunc() 的方式如下

asyncFunc()
.then(result => { ··· })
.catch(error => { ··· });

25.1.1 串接 then() 呼叫

then() 始終傳回 Promise,讓您可以串接方法呼叫

asyncFunc1()
.then(result1 => {
    // Use result1
    return asyncFunction2(); // (A)
})
.then(result2 => { // (B)
    // Use result2
})
.catch(error => {
    // Handle errors of asyncFunc1() and asyncFunc2()
});

then() 傳回的 Promise P 如何解決,取決於其回呼執行的動作

此外,請注意 catch() 如何處理兩個非同步函數呼叫(asyncFunction1()asyncFunction2())的錯誤。也就是說,未捕捉的錯誤會持續傳遞,直到出現錯誤處理常式。

25.1.2 平行執行非同步函數

如果您透過 then() 串接非同步函數呼叫,它們會依序執行,一次一個

asyncFunc1()
.then(() => asyncFunc2());

如果你不這樣做,而是立即呼叫它們,它們基本上會並行執行(在 Unix 程序術語中稱為 fork

asyncFunc1();
asyncFunc2();

Promise.all() 讓你可以在所有結果都出來後收到通知(在 Unix 程序術語中稱為 join)。它的輸入是 Promise 陣列,它的輸出是單一 Promise,並以結果陣列來完成。

Promise.all([
    asyncFunc1(),
    asyncFunc2(),
])
.then(([result1, result2]) => {
    ···
})
.catch(err => {
    // Receives first rejection among the Promises
    ···
});

25.1.3 詞彙表:Promises

Promise API 關於非同步傳遞結果。Promise 物件(簡稱:Promise)是結果的替身,透過該物件傳遞。

狀態

對狀態變更做出反應

變更狀態:有兩個變更 Promise 狀態的操作。在呼叫其中一個操作一次後,後續呼叫不會產生任何效果。

25.2 簡介:Promises

Promises 是一種模式,有助於一種特定類型的非同步程式設計:非同步傳回單一結果的函式(或方法)。接收此類結果的一種流行方式是透過回呼(「回呼作為延續」)

asyncFunction(arg1, arg2,
    result => {
        console.log(result);
    });

承諾提供更好的方式來處理回呼:現在非同步函數會傳回一個承諾,這是一個物件,用作最終結果的佔位符和容器。透過承諾方法 then() 註冊的回呼會收到結果的通知

asyncFunction(arg1, arg2)
.then(result => {
    console.log(result);
});

與作為延續的回呼相比,承諾具有以下優點

25.3 第一個範例

讓我們來看一個第一個範例,讓您了解使用承諾的感覺。

使用 Node.js 風格的回呼,非同步讀取檔案看起來像這樣

fs.readFile('config.json',
    function (error, text) {
        if (error) {
            console.error('Error while reading config file');
        } else {
            try {
                const obj = JSON.parse(text);
                console.log(JSON.stringify(obj, null, 4));
            } catch (e) {
                console.error('Invalid JSON in file');
            }
        }
    });

使用承諾,相同的功能會像這樣使用

readFilePromisified('config.json')
.then(function (text) { // (A)
    const obj = JSON.parse(text);
    console.log(JSON.stringify(obj, null, 4));
})
.catch(function (error) { // (B)
    // File read error or JSON SyntaxError
    console.error('An error occurred', error);
});

仍然有回呼,但它們是透過在結果上呼叫的方法提供的(then()catch())。B 行中的錯誤回呼在兩個方面很方便:首先,它是一種處理錯誤的單一風格(與前一個範例中的 if (error)try-catch 相比)。其次,您可以從單一位置處理 readFilePromisified() 和 A 行中回呼的錯誤。

readFilePromisified() 的程式碼 稍後會說明

25.4 了解承諾的三種方式

讓我們來看了解承諾的三種方式。

以下程式碼包含一個基於承諾的函數 asyncFunc() 及其呼叫。

function asyncFunc() {
    return new Promise((resolve, reject) => { // (A)
        setTimeout(() => resolve('DONE'), 100); // (B)
    });
}
asyncFunc()
.then(x => console.log('Result: '+x));

// Output:
// Result: DONE

asyncFunc() 傳回 Promise。一旦非同步運算的實際結果 'DONE' 準備好,它會透過 resolve() (B 行) 傳送,而 resolve() 是 A 行開始的回呼函式的參數。

那麼 Promise 是什麼?

25.4.1 概念上:呼叫基於 Promise 的函式是封鎖的

以下程式碼從非同步函式 main() 呼叫 asyncFunc()非同步函式 是 ECMAScript 2017 的功能。

async function main() {
    const x = await asyncFunc(); // (A)
    console.log('Result: '+x); // (B)

    // Same as:
    // asyncFunc()
    // .then(x => console.log('Result: '+x));
}
main();

main() 的主體清楚地表達了概念上發生了什麼事,我們通常如何思考非同步運算。也就是說,asyncFunc() 是封鎖函式呼叫

在 ECMAScript 6 和產生器之前,您無法暫停和繼續程式碼。這就是為什麼對於 Promise,您將程式碼繼續之後發生的一切放入回呼函式的原因。呼叫該回呼函式與繼續程式碼相同。

25.4.2 Promise 是非同步傳送值的容器

如果函式傳回 Promise,則該 Promise 就如同一個空白,函式會在計算出結果後(通常)填入結果。您可以透過陣列模擬此程序的簡化版本

function asyncFunc() {
    const blank = [];
    setTimeout(() => blank.push('DONE'), 100);
    return blank;
}
const blank = asyncFunc();
// Wait until the value has been filled in
setTimeout(() => {
    const x = blank[0]; // (A)
    console.log('Result: '+x);
}, 200);

使用 Promise 時,您不會透過 [0](如 A 行所示)存取最終值,而是使用 then() 方法和回呼函式。

25.4.3 Promise 是事件發射器

檢視 Promise 的另一種方式是將它視為發射事件的物件。

function asyncFunc() {
    const eventEmitter = { success: [] };
    setTimeout(() => { // (A)
        for (const handler of eventEmitter.success) {
            handler('DONE');
        }
    }, 100);
    return eventEmitter;
}
asyncFunc()
.success.push(x => console.log('Result: '+x)); // (B)

註冊事件監聽器(第 B 行)可以在呼叫 asyncFunc() 之後執行,因為傳遞給 setTimeout() 的回呼函式(第 A 行)會非同步執行(在此段程式碼完成之後)。

一般事件發射器專門用於傳送多個事件,在您註冊後立即開始。

相對地,Promise 專門用於傳送一個值,並內建防範太晚註冊的保護機制:Promise 的結果會快取,並傳遞給在 Promise 解決後註冊的事件監聽器。

25.5 建立和使用 Promise

讓我們看看 Promise 如何從生產者和消費者端操作。

25.5.1 產生 Promise

作為生產者,您建立一個 Promise 並透過它傳送結果

const p = new Promise(
    function (resolve, reject) { // (A)
        ···
        if (···) {
            resolve(value); // success
        } else {
            reject(reason); // failure
        }
    });

25.5.2 Promise 的狀態

透過 Promise 傳送結果後,Promise 會鎖定在該結果。這表示每個 Promise 始終處於三個(互斥)狀態之一

如果 Promise 已完成或已拒絕,則該 Promise 會解決(它所代表的計算已完成)。Promise 只會解決一次,然後保持已解決狀態。後續的解決嘗試不會產生任何效果。

new Promise() 的參數(從第 A 行開始)稱為執行器

如果在執行器內部引發例外狀況,p 會以該例外狀況拒絕。

25.5.3 使用 Promise

作為 promise 的使用者,您會透過反應(您使用 then()catch() 方法註冊的回呼函式)收到完成或拒絕的通知

promise
.then(value => { /* fulfillment */ })
.catch(error => { /* rejection */ });

承諾對非同步函數(一次性結果)如此有用的原因,在於一旦承諾已解決,它就不會再改變。此外,永遠不會有任何競爭條件,因為無論您在承諾解決之前或之後呼叫 then()catch() 都無所謂

請注意,catch() 只是呼叫 then() 的一個更方便(且建議)的替代方案。也就是說,以下兩個呼叫是等效的

promise.then(
    null,
    error => { /* rejection */ });

promise.catch(
    error => { /* rejection */ });

25.5.4 承諾總是是非同步的

承諾函式庫可以完全控制是否同步(立即)或非同步(在當前延續、當前程式碼片段完成後)將結果傳遞給承諾反應。然而,Promises/A+ 規範要求總是使用後一種執行模式。它透過以下 then() 方法的 需求 (2.2.4) 說明這一點

onFulfilledonRejected 必須等到執行內容堆疊僅包含平台程式碼時才能呼叫。

這表示您的程式碼可以依賴執行完成語意(如 前一章 所述),而且連鎖承諾不會讓其他任務缺乏處理時間。

此外,此限制可防止您撰寫有時立即傳回結果、有時非同步傳回結果的函數。這是一種反模式,因為它會讓程式碼難以預測。如需更多資訊,請參閱 Isaac Z. Schlueter 的「為非同步設計 API」。

25.6 範例

在我們深入探討承諾之前,讓我們在幾個範例中使用我們到目前為止所學到的內容。

25.6.1 範例:將 fs.readFile() 承諾化

以下程式碼是內建 Node.js 函數 fs.readFile() 的基於承諾的版本。

import {readFile} from 'fs';

function readFilePromisified(filename) {
    return new Promise(
        function (resolve, reject) {
            readFile(filename, { encoding: 'utf8' },
                (error, data) => {
                    if (error) {
                        reject(error);
                    } else {
                        resolve(data);
                    }
                });
        });
}

readFilePromisified() 的用法如下

readFilePromisified(process.argv[2])
.then(text => {
    console.log(text);
})
.catch(error => {
    console.log(error);
});

25.6.2 範例:將 XMLHttpRequest 轉為 Promise

以下是一個基於 Promise 的函式,它透過基於事件的 XMLHttpRequest API 執行 HTTP GET

function httpGet(url) {
    return new Promise(
        function (resolve, reject) {
            const request = new XMLHttpRequest();
            request.onload = function () {
                if (this.status === 200) {
                    // Success
                    resolve(this.response);
                } else {
                    // Something went wrong (404 etc.)
                    reject(new Error(this.statusText));
                }
            };
            request.onerror = function () {
                reject(new Error(
                    'XMLHttpRequest Error: '+this.statusText));
            };
            request.open('GET', url);
            request.send();
        });
}

以下是 httpGet() 的使用方法

httpGet('http://example.com/file.txt')
.then(
    function (value) {
        console.log('Contents: ' + value);
    },
    function (reason) {
        console.error('Something went wrong', reason);
    });

25.6.3 範例:延遲活動

我們將 setTimeout() 實作為基於 Promise 的函式 delay()(類似於 Q.delay())。

function delay(ms) {
    return new Promise(function (resolve, reject) {
        setTimeout(resolve, ms); // (A)
    });
}

// Using delay():
delay(5000).then(function () { // (B)
    console.log('5 seconds have passed!')
});

請注意,在 A 行中,我們呼叫 resolve 而沒有參數,這等同於呼叫 resolve(undefined)。我們也不需要 B 行中的完成值,因此只需忽略它。在此,只要收到通知就已足夠。

25.6.4 範例:讓 Promise 超時

function timeout(ms, promise) {
    return new Promise(function (resolve, reject) {
        promise.then(resolve);
        setTimeout(function () {
            reject(new Error('Timeout after '+ms+' ms')); // (A)
        }, ms);
    });
}

請注意,逾時後的拒絕(在 A 行中)不會取消要求,但會阻止 Promise 以其結果完成。

以下是 timeout() 的使用方法

timeout(5000, httpGet('http://example.com/file.txt'))
.then(function (value) {
    console.log('Contents: ' + value);
})
.catch(function (reason) {
    console.error('Error or timeout', reason);
});

25.7 建立 Promise 的其他方式

現在我們準備深入探討 Promise 的功能。我們先來探討建立 Promise 的另外兩種方式。

25.7.1 Promise.resolve()

Promise.resolve(x) 的運作方式如下

這表示您可以使用 Promise.resolve() 將任何值(Promise、thenable 或其他)轉換為 Promise。事實上,Promise.all()Promise.race() 會使用它將任意值的陣列轉換為 Promise 的陣列。

25.7.2 Promise.reject()

Promise.reject(err) 會傳回一個以 err 拒絕的 Promise

const myError = new Error('Problem!');
Promise.reject(myError)
.catch(err => console.log(err === myError)); // true

25.8 串連 Promise

在本節中,我們將仔細探討如何串連 Promise。方法呼叫的結果

P.then(onFulfilled, onRejected)

是一個新的 Promise Q。這表示您可以透過呼叫 Q 的 then() 來維持基於 Promise 的控制流程

25.8.1 使用一般值解析 Q

如果您使用一般值解析 then() 傳回的 Promise Q,您可以透過後續的 then() 來取得該值

asyncFunc()
.then(function (value1) {
    return 123;
})
.then(function (value2) {
    console.log(value2); // 123
});

25.8.2 使用 thenable 解析 Q

您也可以使用 thenable R 來解析 then() 傳回的 Promise Q。thenable 是任何具有方法 then() 的物件,其運作方式類似於 Promise.prototype.then()。因此,Promise 是 thenable。使用 R 解析(例如,從 onFulfilled 傳回 R)表示它會插入在 Q「之後」:R 的結算會轉發到 Q 的 onFulfilledonRejected 回呼。在某種程度上,Q 會變成 R。

此機制的用途主要是扁平化嵌套的 then() 呼叫,如下列範例所示

asyncFunc1()
.then(function (value1) {
    asyncFunc2()
    .then(function (value2) {
        ···
    });
})

扁平化版本如下所示

asyncFunc1()
.then(function (value1) {
    return asyncFunc2();
})
.then(function (value2) {
    ···
})

25.8.3 onRejected 解析 Q

您在錯誤處理常式中傳回的任何內容都會變成履行值(而不是拒絕值!)。這允許您指定在失敗時使用的預設值

retrieveFileName()
.catch(function () {
    // Something went wrong, use a default value
    return 'Untitled.txt';
})
.then(function (fileName) {
    ···
});

25.8.4 擲回例外狀況來拒絕 Q

then()catch() 的回呼中擲回的例外狀況會作為拒絕傳遞到下一個錯誤處理常式

asyncFunc()
.then(function (value) {
    throw new Error();
})
.catch(function (reason) {
    // Handle error here
});

25.8.5 串連和錯誤

可能會有一個或多個沒有錯誤處理常式的 then() 方法呼叫。然後,錯誤會一直傳遞,直到出現錯誤處理常式為止。

asyncFunc1()
.then(asyncFunc2)
.then(asyncFunc3)
.catch(function (reason) {
    // Something went wrong above
});

25.9 常見的 Promise 串連錯誤

25.9.1 錯誤:遺失 Promise 鏈的尾端

在下列程式碼中,建立了兩個 Promise 的鏈,但只傳回了鏈的第一部分。因此,鏈的尾端遺失了。

// Don’t do this
function foo() {
    const promise = asyncFunc();
    promise.then(result => {
        ···
    });

    return promise;
}

這可以透過傳回鏈的尾端來修正

function foo() {
    const promise = asyncFunc();
    return promise.then(result => {
        ···
    });
}

如果你不需要變數 promise,你可以進一步簡化這段程式碼

function foo() {
    return asyncFunc()
    .then(result => {
        ···
    });
}

25.9.2 錯誤:巢狀 Promise

在以下程式碼中,asyncFunc2() 的呼叫是巢狀的

// Don’t do this
asyncFunc1()
.then(result1 => {
    asyncFunc2()
    .then(result2 => {
        ···
    });
});

修正方法是取消巢狀,透過傳回第一個 then() 的第二個 Promise,並透過第二個鏈結的 then() 來處理

asyncFunc1()
.then(result1 => {
    return asyncFunc2();
})
.then(result2 => {
    ···
});

25.9.3 錯誤:建立 Promise 而不是鏈結

在以下程式碼中,方法 insertInto() 為其結果建立一個新的 Promise(第 A 行)

// Don’t do this
class Model {
    insertInto(db) {
        return new Promise((resolve, reject) => { // (A)
          db.insert(this.fields) // (B)
          .then(resultCode => {
              this.notifyObservers({event: 'created', model: this});
              resolve(resultCode); // (C)
          }).catch(err => {
              reject(err); // (D)
          })
        });
    }
    ···
}

如果你仔細觀察,你可以看到結果 Promise 主要用於轉發非同步方法呼叫 db.insert()(第 B 行)的達成(第 C 行)和拒絕(第 D 行)。

修正方法是不建立 Promise,而是依賴 then() 和鏈結

class Model {
    insertInto(db) {
        return db.insert(this.fields) // (A)
        .then(resultCode => {
            this.notifyObservers({event: 'created', model: this});
            return resultCode; // (B)
        });
    }
    ···
}

說明

25.9.4 錯誤:使用 then() 進行錯誤處理

原則上,catch(cb)then(null, cb) 的縮寫。但同時使用 then() 的兩個參數可能會造成問題

// Don’t do this
asyncFunc1()
.then(
    value => { // (A)
        doSomething(); // (B)
        return asyncFunc2(); // (C)
    },
    error => { // (D)
        ···
    });

拒絕回呼(第 D 行)會接收 asyncFunc1() 的所有拒絕,但它不會接收達成回呼(第 A 行)建立的拒絕。例如,第 B 行的同步函式呼叫可能會擲回例外,或第 C 行的非同步函式呼叫可能會產生拒絕。

因此,最好將拒絕回呼移到鏈結的 catch()

asyncFunc1()
.then(value => {
    doSomething();
    return asyncFunc2();
})
.catch(error => {
    ···
});

25.10 錯誤處理提示

25.10.1 操作錯誤與程式設計師錯誤

在程式中,有兩種錯誤

25.10.1.1 操作錯誤:不要混用拒絕和例外

對於操作錯誤,每個函數都應該支援一種錯誤訊號傳遞方式。對於基於 Promise 的函數,這表示不要混用拒絕和例外,這等同於說它們不應該拋出例外。

25.10.1.2 程式設計師錯誤:盡快失敗

對於程式設計師錯誤,透過拋出例外,盡快失敗是有意義的

function downloadFile(url) {
    if (typeof url !== 'string') {
        throw new Error('Illegal argument: ' + url);
    }
    return new Promise(···).
}

如果您這樣做,您必須確保您的非同步程式碼可以處理例外。我認為對於理論上可以靜態檢查的斷言和類似事項(例如,透過分析原始碼的 linter)拋出例外是可以接受的。

25.10.2 處理基於 Promise 的函數中的例外

如果在 then()catch() 的回呼函式中拋出例外,這不是問題,因為這兩個方法會將它們轉換為拒絕。

但是,如果您透過執行同步操作來啟動非同步函數,情況就不同了

function asyncFunc() {
    doSomethingSync(); // (A)
    return doSomethingAsync()
    .then(result => {
        ···
    });
}

如果在 A 行拋出例外,整個函數就會拋出例外。這個問題有兩個解決方案。

25.10.2.1 解決方案 1:傳回一個被拒絕的 Promise

您可以捕捉例外並將它們作為被拒絕的 Promise 傳回

function asyncFunc() {
    try {
        doSomethingSync();
        return doSomethingAsync()
        .then(result => {
            ···
        });
    } catch (err) {
        return Promise.reject(err);
    }
}
25.10.2.2 解決方案 2:在回呼函式中執行同步程式碼

您也可以透過 Promise.resolve() 啟動 then() 方法呼叫的鏈,並在回呼函式中執行同步程式碼

function asyncFunc() {
    return Promise.resolve()
    .then(() => {
        doSomethingSync();
        return doSomethingAsync();
    })
    .then(result => {
        ···
    });
}

另一種方法是透過 Promise 建構函式啟動 Promise 鏈

function asyncFunc() {
    return new Promise((resolve, reject) => {
        doSomethingSync();
        resolve(doSomethingAsync());
    })
    .then(result => {
        ···
    });
}

這種方法可以節省一個 tick(同步程式碼會立即執行),但會讓您的程式碼不那麼有規律。

25.10.3 進一步閱讀

本節的來源

25.11 組合 Promise

組合是指利用現有部分建立新事物。我們已經接觸過 Promise 的順序組合:給定兩個 Promise P 和 Q,以下程式碼會產生一個新的 Promise,在 P 完成後執行 Q。

P.then(() => Q)

請注意,這與同步程式碼的分號類似:同步作業 f()g() 的順序組合如下所示。

f(); g()

本節說明組合 Promise 的其他方式。

25.11.1 手動分岔和合併運算

假設您想要平行執行兩個非同步運算,asyncFunc1()asyncFunc2()

// Don’t do this
asyncFunc1()
.then(result1 => {
    handleSuccess({result1});
});
.catch(handleError);

asyncFunc2()
.then(result2 => {
    handleSuccess({result2});
})
.catch(handleError);

const results = {};
function handleSuccess(props) {
    Object.assign(results, props);
    if (Object.keys(results).length === 2) {
        const {result1, result2} = results;
        ···
    }
}
let errorCounter = 0;
function handleError(err) {
    errorCounter++;
    if (errorCounter === 1) {
        // One error means that everything failed,
        // only react to first error
        ···
    }
}

兩個函式呼叫 asyncFunc1()asyncFunc2() 沒有 then() 串接。因此,它們會立即執行,而且或多或少平行執行。執行現在分岔;每個函式呼叫產生一個獨立的「執行緒」。一旦兩個執行緒都完成(有結果或錯誤),執行會合併到 handleSuccess()handleError() 中的單一執行緒。

這種方法的問題在於它涉及太多手動且容易出錯的工作。解決方法是不要自己執行,而是依賴內建方法 Promise.all()

25.11.2 透過 Promise.all() 分岔和合併運算

Promise.all(iterable) 會接受一個 Promise 的 iterable(thenables 和其他值會透過 Promise.resolve() 轉換為 Promise)。一旦全部完成,它會使用其值的陣列來完成。如果 iterable 為空,all() 傳回的 Promise 會立即完成。

Promise.all([
    asyncFunc1(),
    asyncFunc2(),
])
.then(([result1, result2]) => {
    ···
})
.catch(err => {
    // Receives first rejection among the Promises
    ···
});

25.11.3 透過 Promise.all()map()

Promise 的一個好處是許多同步工具仍然有效,因為基於 Promise 的函式會傳回結果。例如,您可以使用陣列方法 map()

const fileUrls = [
    'http://example.com/file1.txt',
    'http://example.com/file2.txt',
];
const promisedTexts = fileUrls.map(httpGet);

promisedTexts 是 Promise 的陣列。我們可以使用前一節中已經接觸過的 Promise.all(),將該陣列轉換為使用結果陣列完成的 Promise。

Promise.all(promisedTexts)
.then(texts => {
    for (const text of texts) {
        console.log(text);
    }
})
.catch(reason => {
    // Receives first rejection among the Promises
});

25.11.4 透過 Promise.race() 設定逾時

Promise.race(iterable) 會接收一個 Promise 的 iterable(thenables 和其他值會透過 Promise.resolve() 轉換為 Promise),並傳回一個 Promise P。第一個已解決的輸入 Promise 會將其解決狀態傳遞給輸出 Promise。如果 iterable 為空,則 race() 傳回的 Promise 永遠不會解決。

舉例來說,我們可以使用 Promise.race() 來實作逾時

Promise.race([
    httpGet('http://example.com/file.txt'),
    delay(5000).then(function () {
        throw new Error('Timed out')
    });
])
.then(function (text) { ··· })
.catch(function (reason) { ··· });

25.12 兩個有用的 Promise 附加方法

本節說明 Promise 的兩個有用方法,許多 Promise 函式庫都提供這些方法。它們只會用來進一步示範 Promise,你不應該將它們新增到 Promise.prototype(這種修補只應該由多重填補來執行)。

25.12.1 done()

當你串連多個 Promise 方法呼叫時,你可能會在不知不覺中捨棄錯誤。例如

function doSomething() {
    asyncFunc()
    .then(f1)
    .catch(r1)
    .then(f2); // (A)
}

如果 A 行中的 then() 產生拒絕,它將永遠不會在任何地方被處理。Promise 函式庫 Q 提供了一個方法 done(),用於方法呼叫鏈中的最後一個元素。它會取代最後一個 then()(並有一個到兩個參數)

function doSomething() {
    asyncFunc()
    .then(f1)
    .catch(r1)
    .done(f2);
}

或者,它會插入在最後一個 then() 之後(並有零個參數)

function doSomething() {
    asyncFunc()
    .then(f1)
    .catch(r1)
    .then(f2)
    .done();
}

引用 Q 文件

donethen 使用的黃金法則為:將你的承諾傳回給其他人,或者如果鏈在你這裡結束,請呼叫 done 來終止它。使用 catch 終止是不夠的,因為 catch 處理常式本身可能會擲回一個錯誤。

以下是你在 ECMAScript 6 中實作 done() 的方式

Promise.prototype.done = function (onFulfilled, onRejected) {
    this.then(onFulfilled, onRejected)
    .catch(function (reason) {
        // Throw an exception globally
        setTimeout(() => { throw reason }, 0);
    });
};

儘管 done 的功能顯然有用,但它尚未新增到 ECMAScript 6。這個想法是先探討引擎可以自動偵測多少。根據運作情況,可能需要引入 done()

25.12.2 finally()

有時您想要執行一個動作,而不管是否發生錯誤。例如,在您完成使用資源後進行清理。這就是 Promise 方法 finally() 的用途,它的運作方式很像例外處理中的 finally 子句。它的回呼函式不接收任何參數,但會收到已解決或已拒絕的通知。

createResource(···)
.then(function (value1) {
    // Use resource
})
.then(function (value2) {
    // Use resource
})
.finally(function () {
    // Clean up
});

這是 Domenic Denicola 建議 實作 finally() 的方式

Promise.prototype.finally = function (callback) {
    const P = this.constructor;
    // We don’t invoke the callback in here,
    // because we want then() to handle its exceptions
    return this.then(
        // Callback fulfills => continue with receiver’s fulfillment or rejec\
tion
        // Callback rejects => pass on that rejection (then() has no 2nd para\
meter!)
        value  => P.resolve(callback()).then(() => value),
        reason => P.resolve(callback()).then(() => { throw reason })
    );
};

回呼函式決定如何處理接收器 (this) 的解決

範例 1 (由 Jake Archibald 提供):使用 finally() 隱藏一個 spinner。簡化版本

showSpinner();
fetchGalleryData()
.then(data => updateGallery(data))
.catch(showNoDataError)
.finally(hideSpinner);

範例 2 (由 Kris Kowal 提供):使用 finally() 移除一個測試。

const HTTP = require("q-io/http");
const server = HTTP.Server(app);
return server.listen(0)
.then(function () {
    // run test
})
.finally(server.stop);

25.13 Node.js:使用基於回呼函式的同步函式搭配 Promise

Promise 函式庫 Q 有 工具函式,用於與 Node.js 風格的 (err, result) 回呼函式 API 介接。例如,denodeify 會將一個基於回呼函式的函式轉換成一個基於 Promise 的函式

const readFile = Q.denodeify(FS.readFile);

readFile('foo.txt', 'utf-8')
.then(function (text) {
    ···
});

denodify 是個微型函式庫,它只提供 Q.denodeify() 的功能,並符合 ECMAScript 6 Promise API。

25.14 相容於 ES6 的 Promise 函式庫

市面上有很多 Promise 函式庫。以下這些函式庫符合 ECMAScript 6 API,這表示您可以立即使用它們,並在稍後輕鬆地移轉到原生 ES6。

極簡的 polyfill

較大型的 Promise 函式庫

ES6 標準函式庫的 polyfill

25.15 下一步:透過產生器使用 Promise

透過 Promise 實作非同步函式比透過事件或回呼更方便,但仍然不是理想的作法

解決方案是將封鎖呼叫帶入 JavaScript。產生器讓我們透過函式庫來做到這一點:在以下程式碼中,我使用 控制流程函式庫 co 來非同步地擷取兩個 JSON 檔案。

co(function* () {
    try {
        const [croftStr, bondStr] = yield Promise.all([  // (A)
            getFile('https://127.0.0.1:8000/croft.json'),
            getFile('https://127.0.0.1:8000/bond.json'),
        ]);
        const croftJson = JSON.parse(croftStr);
        const bondJson = JSON.parse(bondStr);

        console.log(croftJson);
        console.log(bondJson);
    } catch (e) {
        console.log('Failure to read: ' + e);
    }
});

在 A 行中,執行透過 yield 封鎖(等待),直到 Promise.all() 的結果準備好。這表示程式碼在執行非同步操作時看起來像是同步的。

詳細說明請參閱 產生器章節

25.16 深入探討 Promise:一個簡單的實作

在本節中,我們將從不同的角度探討 Promise:我們不會學習如何使用 API,而是會探討它的簡單實作。這個不同的角度對我理解 Promise 有很大的幫助。

Promise 實作稱為 DemoPromise。為了更容易理解,它並未完全符合 API。但它已經足夠接近,可以讓您深入了解實際實作所面臨的挑戰。

DemoPromise 是具有三個原型方法的類別

也就是說,resolvereject 是方法(相對於傳遞給建構函式回呼參數的函式)。

25.16.1 獨立的 Promise

我們的首要實作是具有最小功能的獨立 Promise

以下是此首要實作的使用方式

const dp = new DemoPromise();
dp.resolve('abc');
dp.then(function (value) {
    console.log(value); // abc
});

下圖說明了我們的首個 DemoPromise 如何運作

25.16.1.1 DemoPromise.prototype.then()

讓我們先檢視 then()。它必須處理兩種情況

then(onFulfilled, onRejected) {
    const self = this;
    const fulfilledTask = function () {
        onFulfilled(self.promiseResult);
    };
    const rejectedTask = function () {
        onRejected(self.promiseResult);
    };
    switch (this.promiseState) {
        case 'pending':
            this.fulfillReactions.push(fulfilledTask);
            this.rejectReactions.push(rejectedTask);
            break;
        case 'fulfilled':
            addToTaskQueue(fulfilledTask);
            break;
        case 'rejected':
            addToTaskQueue(rejectedTask);
            break;
    }
}

前一個程式碼片段使用下列輔助函式

function addToTaskQueue(task) {
    setTimeout(task, 0);
}
25.16.1.2 DemoPromise.prototype.resolve()

resolve() 的運作方式如下:如果 Promise 已解決,則它不會執行任何動作(確保 Promise 只會解決一次)。否則,Promise 的狀態會變更為 'fulfilled',且結果會快取在 this.promiseResult 中。接著,觸發迄今已排隊的所有履行反應。

resolve(value) {
    if (this.promiseState !== 'pending') return;
    this.promiseState = 'fulfilled';
    this.promiseResult = value;
    this._clearAndEnqueueReactions(this.fulfillReactions);
    return this; // enable chaining
}
_clearAndEnqueueReactions(reactions) {
    this.fulfillReactions = undefined;
    this.rejectReactions = undefined;
    reactions.map(addToTaskQueue);
}

reject() 類似於 resolve()

25.16.2 串連

我們實作的下一項功能是串連

顯然地,只有 then() 會變更

then(onFulfilled, onRejected) {
    const returnValue = new Promise(); // (A)
    const self = this;

    let fulfilledTask;
    if (typeof onFulfilled === 'function') {
        fulfilledTask = function () {
            const r = onFulfilled(self.promiseResult);
            returnValue.resolve(r); // (B)
        };
    } else {
        fulfilledTask = function () {
            returnValue.resolve(self.promiseResult); // (C)
        };
    }

    let rejectedTask;
    if (typeof onRejected === 'function') {
        rejectedTask = function () {
            const r = onRejected(self.promiseResult);
            returnValue.resolve(r); // (D)
        };
    } else {
        rejectedTask = function () {
            // `onRejected` has not been provided
            // => we must pass on the rejection
            returnValue.reject(self.promiseResult); // (E)
        };
    }
    ···
    return returnValue; // (F)
}

then() 會建立並傳回一個新的 Promise(第 A 和 F 行)。此外,fulfilledTaskrejectedTask 的設定方式不同:在解決後…

25.16.3 扁平化

扁平化主要是讓串接更方便:通常,從反應傳回值會將它傳遞給下一個 then()。如果我們傳回一個 Promise,最好可以為我們「解開」,就像以下範例

asyncFunc1()
.then(function (value1) {
    return asyncFunc2(); // (A)
})
.then(function (value2) {
    // value2 is fulfillment value of asyncFunc2() Promise
    console.log(value2);
});

我們在第 A 行傳回一個 Promise,而且不必在目前方法內巢狀呼叫 then(),我們可以在方法的結果上呼叫 then()。因此:沒有巢狀 then(),一切都保持扁平。

我們透過讓 resolve() 方法進行扁平化來實作這一點

如果我們允許 Q 成為 thenable(而不仅仅是 Promise),我們可以讓扁平化更通用。

為了實作鎖定,我們引入一個新的布林旗標 this.alreadyResolved。一旦為 true,this 會被鎖定,而且無法再解決。請注意,this 仍可能處於待處理狀態,因為它的狀態現在與它鎖定的 Promise 相同。

resolve(value) {
    if (this.alreadyResolved) return;
    this.alreadyResolved = true;
    this._doResolve(value);
    return this; // enable chaining
}

實際的解決現在發生在私有方法 _doResolve()

_doResolve(value) {
    const self = this;
    // Is `value` a thenable?
    if (typeof value === 'object' && value !== null && 'then' in value) {
        // Forward fulfillments and rejections from `value` to `this`.
        // Added as a task (versus done immediately) to preserve async semant\
ics.
        addToTaskQueue(function () { // (A)
            value.then(
                function onFulfilled(result) {
                    self._doResolve(result);
                },
                function onRejected(error) {
                    self._doReject(error);
                });
        });
    } else {
        this.promiseState = 'fulfilled';
        this.promiseResult = value;
        this._clearAndEnqueueReactions(this.fulfillReactions);
    }
}

扁平化在第 A 行執行:如果 value 已完成,我們希望 self 已完成,如果 value 已拒絕,我們希望 self 已拒絕。轉發透過私有方法 _doResolve_doReject 發生,以繞過 alreadyResolved 的保護。

25.16.4 更詳細的 Promise 狀態

使用串接後,Promise 的狀態會變得更複雜(如 ECMAScript 6 規範的 第 25.4 節 所述)

如果你只是使用 Promise,通常可以採用簡化的世界觀,並忽略鎖定。最重要的狀態相關概念仍然是「已結算」:如果 Promise 已完成或已拒絕,則表示已結算。Promise 結算後,它就不會再改變(狀態和完成或拒絕值)。

如果你想實作 Promise,那麼「解決」也很重要,而且現在更難理解

25.16.5 例外

作為我們的最後一個特徵,我們希望我們的 Promise 能夠將使用者程式碼中的例外處理為拒絕。現在,「使用者程式碼」表示 then() 的兩個回呼參數。

以下摘錄顯示我們如何將 onFulfilled 內部的例外轉換為拒絕 – 通過在 A 行中的呼叫周圍包裝一個 try-catch

then(onFulfilled, onRejected) {
    ···
    let fulfilledTask;
    if (typeof onFulfilled === 'function') {
        fulfilledTask = function () {
            try {
                const r = onFulfilled(self.promiseResult); // (A)
                returnValue.resolve(r);
            } catch (e) {
                returnValue.reject(e);
            }
        };
    } else {
        fulfilledTask = function () {
            returnValue.resolve(self.promiseResult);
        };
    }
    ···
}

25.16.6 揭示建構函式模式

如果我們想將 DemoPromise 轉換為實際的 Promise 實作,我們仍然需要實作 揭示建構函式模式 [2]:ES6 Promise 不是透過方法解決和拒絕,而是透過傳遞給 執行器(建構函式的回呼參數)的函式。

如果執行器擲出例外,則「它的」Promise 必須被拒絕。

25.17 Promise 的優點和限制

25.17.1 Promise 的優點

25.17.1.1 統一非同步 API

Promise 的一個重要優點是,非同步瀏覽器 API 將越來越多地使用它們,並統一當前多樣且不兼容的模式和慣例。讓我們來看兩個即將推出的基於 Promise 的 API。

fetch API 是 XMLHttpRequest 的基於 Promise 的替代方案

fetch(url)
.then(request => request.text())
.then(str => ···)

fetch() 為實際請求傳回 Promise,text() 為內容傳回 Promise,內容為字串。

用於程式化匯入模組的 ECMAScript 6 API 也基於 Promise

System.import('some_module.js')
.then(some_module => {
    ···
})
25.17.1.2 Promise 與事件

與事件相比,Promise 更適合處理一次性結果。無論您是在計算結果之前還是之後註冊結果,您都將獲得結果。Promise 的這種優勢在性質上是基本的。另一方面,您不能使用它們來處理重複發生的事件。串連是 Promise 的另一個優點,但可以將其新增到事件處理中。

25.17.1.3 Promise 與回呼

相較於回呼,Promise 具有更簡潔的函數(或方法)簽章。使用回呼時,參數用於輸入和輸出

fs.readFile(name, opts?, (err, string | Buffer) => void)

使用 Promise 時,所有參數都用於輸入

readFilePromisified(name, opts?) : Promise<string | Buffer>

Promise 的其他優點包括

25.17.2 Promise 並非總是最佳選擇

Promise 適用於單一非同步結果。它們不適合於

ECMAScript 6 Promise 缺少兩個有時很有用的功能

Q Promise 函式庫 支援 後者,而且有 計畫 將這兩個功能新增到 Promises/A+。

25.18 參考:ECMAScript 6 Promise API

本節概述 ECMAScript 6 Promise API,如 規格 中所述。

25.18.1 Promise 建構函式

Promise 的建構函式呼叫如下

const p = new Promise(function (resolve, reject) { ··· });

此建構函式的回呼稱為執行器。執行器可以使用其參數來解析或拒絕新的 Promise p

25.18.2 靜態 Promise 方法

25.18.2.1 建立 Promises

下列兩個靜態方法建立接收者的新執行個體

25.18.2.2 組合 Promises

直覺上,靜態方法 Promise.all()Promise.race() 將 Promises 的可迭代組合成一個 Promise。也就是說

這些方法為

25.18.3 Promise.prototype 方法

25.18.3.1 Promise.prototype.then(onFulfilled, onRejected)

遺漏反應的預設值可以這樣實作

function defaultOnFulfilled(x) {
    return x;
}
function defaultOnRejected(e) {
    throw e;
}
25.18.3.2 Promise.prototype.catch(onRejected)

25.19 進一步閱讀

[1] “Promises/A+”,由 Brian Cavalier 和 Domenic Denicola 編輯(JavaScript Promises 的實際標準)

[2] “The Revealing Constructor Pattern” by Domenic Denicola(此模式由 Promise 建構函式使用)

下一頁:VI 雜項