Promise 基础

Promise 是为异步操作的结果所准备的占位符。函数可以返回一个 Promise,而不必订阅一个事件或向函数传递一个回调参数,就像这样:

// readFile promises to complete at some point in the future
let promise = readFile("example.txt");

在此代码中,readFile() 实际上并未立即开始读取文件,这将会在稍后发生。此函数反而会返回一个 Promise 对象以表示异步读取操作,因此你可以在将来再操作它。你能对结果进行操作的确切时刻,完全取决于 Promise 的生命周期是如何进行的。

Promise 的生命周期

每个 Promise 都会经历一个短暂的生命周期,初始为挂起态(pending state),这表示异步操作尚未结束。一个挂起的 Promise 也被认为是未决的(unsettled)。上个例子中的 Promise 在 readFile() 函数返回它的时候就是处在挂起态。一旦异步操作结束,Promise 就会被认为是已决的(settled),并进入两种可能状态之一:

  1. 已完成(fulfilled):Promise 的异步操作已成功结束;

  2. 已拒绝(rejected):Promise 的异步操作未成功结束,可能是一个错误, 或由其他原因导致。

内部的 [[PromiseState]] 属性会被设置为 "pending" 、 "fulfilled" 或 "rejected",以反映 Promise 的状态。该属性并未在 Promise 对象上被暴露出来,因此你无法以编程方式判断 Promise 到底处于哪种状态。不过你可以使用 then() 方法在 Promise 的状态改变时执行一些特定操作。

译注: 相关词汇翻译汇总

Promise 是相对比较新的一个概念,相关的许多词汇有一定的交叉性,并且在翻译为中文时可能有些并不太容易分辨。因此涉及 Promise 的许多资料都对相关大部分词汇不作翻译,直接使用英文原词。

译者在本章斗胆对几乎所有词汇进行了翻译,如有不妥,欢迎指出。此处是词汇翻译的汇总,以便参考:

  1. pending:挂起,表示未结束的 Promise 状态。相关词汇 “挂起态”。

  2. fulfilled:已完成,表示已成功结束的 Promise 状态,可以理解为 “成功完成”。相关词汇 “完成”、“被完成”、“完成态”。

  3. rejected:已拒绝,表示已结束但失败的 Promise 状态。相关词汇 “拒绝”、“被拒绝”、“拒绝态”。

  4. resolve:决议,表示将 Promise 推向成功态,可以理解为 “决议通过”,在 Promise 概念中与 “完成” 是近义词。相关词汇 “决议态”、“已决议”、“被决议”。

  5. unsettled:未决,或者称为 “未解决”,表示 Promise 尚未被完成或拒绝, 与 “挂起” 是近义词。

  6. settled:已决,或者称为 “已解决”,表示 Promise 已被完成或拒绝。注意这与 “已完成” 或 “已决议” 不同,“已决” 的状态也可能是 “拒绝态”(已失败)。

  7. fulfillment handler:完成处理函数,表示 Promise 为完成态时会被调用的函数。

  8. rejection handler:拒绝处理函数,表示 Promise 为拒绝态时会被调用的函数。

then() 方法在所有的 Promise 上都存在,并且接受两个参数。第一个参数是 Promise 被完成时要调用的函数与异步操作关联的任何附加数据都会被传入这个完成函数。第二个参数则是 Promise 被拒绝时要调用的函数,与完成函数相似,拒绝函数会被传入与拒绝相关联的任何附加数据。

用这种方式实现 then() 方法的任何对象都被称为一个 thenable。所有的 Promise 都是 thenable,反之则未必成立。

传递给 then() 的两个参数都是可选的,因此你可以监听完成与拒绝的任意组合形式。例如,研究这组 then() 调用:

let promise = readFile("example.txt");

promise.then(function(contents) {
    // fulfillment
    console.log(contents);
}, function(err) {
    // rejection
    console.error(err.message);
});

promise.then(function(contents) {
    // fulfillment
    console.log(contents);
});

promise.then(null, function(err) {
    // rejection
    console.error(err.message);
});

这三个 then() 调用都操作在同一个 Promise 上。第一个调用同时监听了完成与失败;第二个调用只监听了完成,错误不会被报告;第三个则只监听了拒绝,并不报告成功信息。

Promis 也具有一个 catch() 方法,其行为等同于只传递拒绝处理函数给 then()。例如,以下的 catch() 与 then() 调用是功能等效的。

promise.catch(function(err) {
    // rejection
    console.error(err.message);
});

// is the same as:

promise.then(null, function(err) {
    // rejection
    console.error(err.message);
});

then() 与 catch() 背后的意图是让你组合使用它们来正确处理异步操作的结果。 此系统要优于事件与回调函数,因为它让操作是成功还是失败变得完全清晰(事件模式倾向于在出错时不被触发,而在回调函数模式中你必须始终记得检查错误参数)。只需知道若你未给 Promise 附加拒绝处理函数,所有的错误就会静默发生。建议始终附加一个拒绝处理函数,即使该处理程序只是用于打印错误日志。

即使完成或拒绝处理函数在 Promise 已经被解决之后才添加到作业队列,它们仍然会被执行。这允许你随时添加新的完成或拒绝处理函数,并保证它们会被调用。例如:

let promise = readFile("example.txt");

// original fulfillment handler
promise.then(function(contents) {
    console.log(contents);

    // now add another
    promise.then(function(contents) {
        console.log(contents);
    });
});

在此代码中,完成处理函数又为同一个 Promise 添加了另一个完成处理函数。这个 Promise 此刻已经完成了,因此新的处理程序就被添加到任务队列,并在就绪时(前面的作业执行完毕后)被调用。拒绝处理函数使用同样方式工作。

每次调用 then() 或 catch() 都会创建一个新的作业,它会在 Promise 已决议时被执行。但这些作业最终会进入一个完全为 Promise 保留的作业队列。这个独立队列的确切细节对于理解如何使用 Promise 是不重要的,你只需理解作业队列通常来说是如何工作的。

创建未决的 Promise

新的 Promise 使用 Promise 构造器来创建。此构造器接受单个参数:一个被称为执行器(executor)的函数,包含初始化 Promise 的代码。该执行器会被传递两个名为 resolve() 与 reject() 的函数作为参数。resolve() 函数在执行器成功结束时被调用,用于示意该 Promise 已经准备好被决议(resolved ),而 reject() 函数则表明执行器的操作已失败。

此处有个范例,在 Node.js 中使用了一个 Promise,实现了本章前面的 readFile() 函数:

// Node.js example

let fs = require("fs");

function readFile(filename) {
    return new Promise(function(resolve, reject) {

        // trigger the asynchronous operation
        fs.readFile(filename, { encoding: "utf8" }, function(err, contents) {

            // check for errors
            if (err) {
                reject(err);
                return;
            }

            // the read succeeded
            resolve(contents);

        });
    });
}

let promise = readFile("example.txt");

// listen for both fulfillment and rejection
promise.then(function(contents) {
    // fulfillment
    console.log(contents);
}, function(err) {
    // rejection
    console.error(err.message);
});

在此例中,Node.js 原生的 fs.readFile() 异步调用被包装在一个 Promise 中。执行器要么传递错误对象给 reject() 函数,要么传递文件内容给 resolve() 函数。

要记住执行器会在 readFile() 被调用时立即运行。当 resolve() 或 reject() 在执行器内部被调用时,一个作业被添加到作业队列中,以便决议(resolve)这个 Promise。这被称为作业调度(job scheduling),若你曾用过 setTimeout() 或 setInterval() 函数,那么应该已经熟悉这种方式。在作业调度中,你添加新作业到队列中是表示:“不要立刻执行这个作业,但要在稍后执行它”。例如,setTimeout() 函数能让你指定一个延迟时间,延迟之后作业才会被添加到队列:

// add this function to the job queue after 500ms have passed
setTimeout(function() {
    console.log("Timeout");
}, 500);

console.log("Hi!");

此代码安排一个作业在 500 毫秒之后被添加到作业队列。此处两个 console.log() 调用产生了以下输出:

Hi!
Timeout

多亏这 500 毫秒的延迟,被传递给 setTimeout() 的匿名函数的输出,被排在了 console.log("Hi!") 输出之后。

译注:实际上前面范例中的输出顺序与 500 毫秒的延时没有关系,而与 setTimeout() 的机制有关。我们可以把延时改为 0,依然会得到相同的结果:

// 在 0 毫秒之后添加此函数到作业队列
setTimeout(function() {
    console.log("Timeout");
}, 0);

console.log("Hi!");

输出结果会保持不变。setTimeout() 确实有延时效果,但原书的例子不当,没有完全说清其中的机制。

Promise 工作方式与之相似。Promise 的执行器会立即执行,早于源代码中在其之后的任何代码。例如:

let promise = new Promise(function(resolve, reject) {
    console.log("Promise");
    resolve();
});

console.log("Hi!");

此代码的输出结果为:

Promise
Hi!

调用 resolve() 触发了一个异步操作。传递给 then() 与 catch() 的函数会异步地被执行,并且它们也被添加到了作业队列(先进队列再执行)。此处有个例子:

let promise = new Promise(function(resolve, reject) {
    console.log("Promise");
    resolve();
});

promise.then(function() {
    console.log("Resolved.");
});

console.log("Hi!");

此例的输出结果为:

Promise
Hi!
Resolved

注意:尽管对 then() 的调用出现在 console.log("Hi!") 代码行之前,它实际上稍后才会执行(与执行器中那行 "Promise" 不同)。这是因为完成处理函数与拒绝处理函数总是会在执行器的操作结束后被添加到作业队列的尾部。

创建已决的 Promise

基于 Promise 执行器行为的动态本质,Promise 构造器就是创建未决的 Promise 的最好方式。但若你想让一个 Promise 代表一个已知的值,那么安排一个单纯传值给 resolve() 函数的作业并没有意义。相反,有两种方法可使用指定值来创建已决的 Promise 。

使用 Promise.resolve()

Promise.resolve() 方法接受单个参数并会返回一个处于完成态的 Promise。 这意味着没有任何作业调度会发生,并且你需要向 Promise 添加一个或更多的完成处理函数来提取这个参数值。例如:

let promise = Promise.resolve(42);

promise.then(function(value) {
    console.log(value);         // 42
});

此代码创建了一个已完成的 Promise,因此完成处理函数就接收到 42 作为 value 参数。若一个拒绝处理函数被添加到此 Promise,该拒绝处理函数将永不会被调用,因为此 Promise 绝不可能再是拒绝态。

使用 Promise.reject()

你也可以使用 Promise.reject() 方法来创建一个已拒绝的 Promise。此方法像 Promise.resolve() 一样工作,区别是被创建的 Promise 处于拒绝态,如下:

let promise = Promise.reject(42);

promise.catch(function(value) {
    console.log(value);         // 42
});

任何附加到这个 Promise 的拒绝处理函数都将会被调用,而完成处理函数则不会执行。

若你传递一个 Promise 给 Promise.resolve() 或 Promise.reject() 方法,该 Promise 会不作修改原样返回。

译注:经过测试,在几大浏览器中都存在与上一句话不符的情况。

  1. 若传入的 Promise 为挂起态,则 Promise.resolve() 调用会将该 Promise 原样返回。此后,若决议原 Promise,在 then() 中可以接收到原例中的参数 42;而若拒绝原 Promise,则在 catch() 中可以接收到参数 42。 但 Promise.reject() 调用则会对原先的 Promise 重新进行包装,对其使用 catch() 可以捕捉到错误,处理函数中的 value 参数不会是数值 42,而是原先处于挂起态的 Promise。

  2. 若传入的 Promise 为完成态,则 Promise.resolve() 调用会将该 Promise 原样返回,在 then() 中可以接收到原例中的参数 42。但 Promise.reject() 调用则会对原先的 Promise 重新进行包装,对其使用 catch() 可以捕捉到错误,处理函数中的 value 参数不会是数值 42,而是原先处于完成态的 Promise。

  3. 若传入的 Promise 为拒绝态,则 Promise.reject() 调用会将该 Promise 原样返回,在 catch() 中可以接收到参数 42。但 Promise.resolve() 调用则会对原先的 Promise 重新进行包装,对其使用 then() 可以进行完成处理,处理函数中的 value 参数不是 42,而是原先处于拒绝态的 Promise。也就是说此时的情况与上一种情况相反。

总结:对挂起态或完成态的 Promise 使用 Promise.resolve() 没问题,会返回原 Promise;对拒绝态的 Promise 使用 Promise.reject() 也没问题。 而除此之外的情况全都会在原 Promise 上包装出一个新的 Promise。

非 Promise 的 Thenable

Promise.resolve() 与 Promise.reject() 都能接受非 Promise 的 thenable 作为参数。当传入了非 Promise 的 thenable 时,这些方法会创建一个新的 Promise,此 Promise 会在 then() 函数之后被调用。

当一个对象拥有一个能接受 resolve 与 reject 参数的 then() 方法,该对象就会被认为是一个非 Promise 的 thenable,就像这样:

let thenable = {
    then: function(resolve, reject) {
        resolve(42);
    }
};

此例中的 thenable 对象,除了 then() 方法之外没有任何与 Promise 相关的特征。你可以调用 Promise.resolve() 来将 thenable 转换为一个已完成的 Promise:

let thenable = {
    then: function(resolve, reject) {
        resolve(42);
    }
};

let p1 = Promise.resolve(thenable);
p1.then(function(value) {
    console.log(value);     // 42
});

在此例中,Promise.resolve() 调用了 thenable.then(),确定了这个 thenable 的 Promise 状态:由于 resolve(42) 在 thenable.then() 方法内部被调用,这个 thenable 的 Promise 状态也就被设为已完成。一个名为 p1 的新 Promise 被创建为完成态,并从 thenable 中接收到了值(此处为 42),于是 p1 的完成处理函数就接收到一个值为 42 的参数。

使用 Promise.resolve(),同样还能从一个 thenable 创建一个已拒绝的 Promise:

let thenable = {
    then: function(resolve, reject) {
        reject(42);
    }
};

let p1 = Promise.resolve(thenable);
p1.catch(function(value) {
    console.log(value);     // 42
});

此例类似于上例,区别是此处的 thenable 被拒绝了。当 thenable.then() 执行时,一个处于拒绝态的新 Promise 被创建,并伴随着一个值(42)。这个值此后会被传递给 p1 的拒绝处理函数。

Promise.resolve() 与 Promise.reject() 用类似方式工作,让你能轻易处理非 Promise 的 thenable。在 Promise 被引入 ES6 之前,许多库都使用了 thenable,因此将 thenable 转换为正规 Promise 的能力就非常重要了, 能对之前已存在的库提供向下兼容。当你不能确定一个对象是否是 Promise 时, 将该对象传递给 Promise.resolve() 或 Promise.reject()(取决于你的预期结果)是能找出的最好方式,因为传入真正的 Promise 只会被直接传递出来,并不会被修改(但请注意前面译注提到的特殊情况) 。

执行器错误

如果在执行器内部抛出了错误,那么 Promise 的拒绝处理函数就会被调用。例如:

let promise = new Promise(function(resolve, reject) {
    throw new Error("Explosion!");
});

promise.catch(function(error) {
    console.log(error.message);     // "Explosion!"
});

在此代码中,执行器故意抛出了一个错误。此处在每个执行器之内并没有显式的 try-catch,因此错误就被捕捉并传递给了拒绝处理函数。这个例子等价于:

let promise = new Promise(function(resolve, reject) {
    try {
        throw new Error("Explosion!");
    } catch (ex) {
        reject(ex);
    }
});

promise.catch(function(error) {
    console.log(error.message);     // "Explosion!"
});

执行器处理程序捕捉了抛出的任何错误,以简化这种常见处理。但在执行器内抛出的错误仅当存在拒绝处理函数时才会被报告,否则这个错误就会被隐瞒。这在开发者早期使用 Promise 的时候是一个问题,但 JS 环境通过提供钩子(hook)来捕捉被拒绝的 Promise,从而解决了此问题。