25. 使用 Promise 进行异步编程
目录
请支持本书:购买 (PDF, EPUB, MOBI)捐赠
(广告,请不要屏蔽。)

25. 使用 Promise 进行异步编程

本章介绍如何通过 Promise 进行异步编程,特别是 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 进程术语中称为“分叉”)

asyncFunc1();
asyncFunc2();

Promise.all() 使您能够在所有结果都返回时收到通知(在 Unix 进程术语中称为“合并”)。 它的输入是一个 Promise 数组,输出是一个 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

Promise 是一种模式,有助于处理一种特定类型的异步编程:异步返回单个结果的函数(或方法)。 接收此类结果的一种常用方法是通过回调(“回调作为延续”)

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

Promise 提供了一种更好的使用回调函数的方法:现在,异步函数返回一个 *Promise*,这是一个充当最终结果的占位符和容器的对象。 通过 Promise 方法 then() 注册的回调函数将在结果返回时收到通知

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

与回调作为延续相比,Promise 具有以下优点

25.3 第一个例子

让我们看第一个例子,让您体验一下使用 Promises 是什么样的。

使用 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');
            }
        }
    });

使用 Promises,相同的功能的使用方式如下

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 理解 Promises 的三种方式

让我们看看理解 Promises 的三种方式。

以下代码包含一个基于 Promise 的函数 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 和生成器之前,您无法挂起和恢复代码。 这就是为什么对于 Promises,您将代码恢复后发生的所有事情都放入回调函数中。 调用该回调函数与恢复代码相同。

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 只能兑现一次,并且一旦兑现就会保持该状态。后续尝试兑现操作将无效。

new Promise() 的参数(从代码行 A 开始)称为 *执行器*。

如果在执行器内部抛出异常,则 p 将以该异常被拒绝。

25.5.3 消费 Promise

作为 promise 的消费者,你会通过 *反应* 被告知兑现或拒绝——你使用 then()catch() 方法注册的回调函数。

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

Promise 之所以对异步函数(具有一次性结果)如此有用,是因为一旦 Promise 兑现,它就不会再改变。此外,永远不会出现竞争条件,因为无论是在 Promise 兑现之前还是之后调用 then()catch() 都无关紧要。

请注意,catch() 只是调用 then() 的一种更方便(也是推荐的)替代方法。也就是说,以下两个调用是等效的:

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

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

25.5.4 Promise 始终是异步的

Promise 库可以完全控制结果是同步(立即)还是异步(在当前延续,即当前代码段完成后)传递给 Promise 反应。但是,Promises/A+ 规范要求始终使用后一种执行模式。它通过 then() 方法的以下要求(2.2.4)来规定这一点:

在执行上下文堆栈仅包含平台代码之前,不得调用 onFulfilledonRejected

这意味着你的代码可以依赖于运行至完成语义(如上一章所述),并且 Promise 链不会使其他任务无法获得处理时间。

此外,此约束还阻止你编写有时立即返回结果、有时异步返回结果的函数。这是一种反模式,因为它会使代码变得不可预测。有关更多信息,请参阅 Isaac Z. Schlueter 的“为异步设计 API”。

25.6 示例

在深入探讨 Promise 之前,让我们在几个示例中使用到目前为止所学到的知识。

25.6.1 示例:将 fs.readFile() Promise 化

以下代码是内置 Node.js 函数 fs.readFile() 的基于 Promise 的版本。

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 返回它)意味着它被插入到 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() 的兑现(代码行 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 => {
        ···
    });
}

这种方法可以为您节省一个计时周期(同步代码会立即执行),但会降低代码的规范性。

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 的可迭代对象(thenable 和其他值通过 Promise.resolve() 转换为 Promise)。一旦所有 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 使用结果数组来完成。

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 的可迭代对象(thenable 和其他值通过 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 中(这种类型的修补应该只由 polyfill 完成)。

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 使用的黄金法则是:要么将您的 Promise 返回给其他人,要么如果链条以您结束,则调用 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() 隐藏微调器。简化版本:

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('http://localhost:8000/croft.json'),
            getFile('http://localhost: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。一旦它为真,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 内部的异常转换为拒绝 - 通过在其调用周围包装一个 try-catch(A 行)。

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 创建 Promise

以下两个静态方法创建其接收者的新实例

25.18.2.2 组合 Promise

直观地说,静态方法 Promise.all()Promise.race() 将 Promise 的可迭代对象组合成单个 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 Promise 的事实标准)

[2] “揭示构造函数模式”,作者 Domenic Denicola(Promise 构造函数使用此模式)

下一页:VI 杂项