使用 Promise - JavaScript | MDN

Using Promises

 

A Promise is an object representing the eventual completion or failure of an asynchronous operation. Since most people are consumers of already-created promises, this guide will explain consumption of returned promises before explaining how to create them.

Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。因为大多数人仅仅是使用已创建的 Promise 实例对象,所以本教程将首先说明怎样使用 Promise,再说明如何创建 Promise。


Essentially, a promise is a returned object to which you attach callbacks, instead of passing callbacks into a function.

本质上Promise是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了。


Imagine a function, createAudioFileAsync(), which asynchronously generates a sound file given a configuration record and two callback functions, one called if the audio file is successfully created, and the other called if an error occurs.

假设现在有一个名为 createAudioFileAsync() 的函数,它接收一些配置和两个回调函数,然后异步地生成音频文件。一个回调函数在文件成功创建时的被调用,另一个则在出现异常时的被调用。


Here's some code that uses createAudioFileAsync():

以下为使用 createAudioFileAsync() 的示例:

 

// 成功的回调函数
function successCallback(result) {
console.log("Audio file ready at URL: " + result);
}

// 失败的回调函数
function failureCallback(error) {
console.error("Error generating audio file: " + error);
}

createAudioFileAsync(audioSettings, successCallback, failureCallback);

 

…modern functions return a promise you can attach your callbacks to instead:

更现代的函数会返回一个 promise 对象,使得你可以将你的回调函数绑定在该 promise 上。


If createAudioFileAsync() were rewritten to return a promise, using it could be as simple as this:

如果函数 createAudioFileAsync() 被重写为返回promise的形式,那么我们可以向下面这样简单地调用它:

const promise = createAudioFileAsync(audioSettings); 
promise.then(successCallback, failureCallback);

// 或者简写为
createAudioFileAsync(audioSettings).then(successCallback, failureCallback);

 

We call this an asynchronous function call. This convention has several advantages. We will explore each one.

我们把这个称为异步函数调用,这种形式有若干优点,下面我们将会逐一讨论。

 

Guarantees

约定

Unlike "old-style", passed-in callbacks, a promise comes with some guarantees:

不同于“老式”的传入回调,在使用 Promise 时,会有以下约定:

  • Callbacks will never be called before the completion of the current run of the JavaScript event loop.
  • 在本轮 事件循环 运行完成之前,回调函数是不会被调用的。
  • Callbacks added with then() even after the success or failure of the asynchronous operation, will be called, as above.
  • 即使异步操作已经完成(成功或失败),在这之后通过 then() 添加的回调函数也会被调用。
  • Multiple callbacks may be added by calling then() several times. Each callback is executed one after another, in the order in which they were inserted.
  • 通过多次调用 then() 可以添加多个回调函数,它们会按照插入顺序执行。

One of the great things about using promises is chaining.
Promise 很棒的一点就是链式调用(chaining)。

 

Chaining

链式调用

A common need is to execute two or more asynchronous operations back to back, where each subsequent operation starts when the previous operation succeeds, with the result from the previous step. We accomplish this by creating a promise chain.

连续执行两个或者多个异步操作是一个常见的需求,在上一个操作执行成功之后,开始下一个的操作,并带着上一步操作所返回的结果。我们可以通过创造一个 Promise 链来实现这种需求。


Here's the magic: the then() function returns a new promise, different from the original:

见证奇迹的时刻:then() 函数会返回一个和原来不同的新的Promise:

const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);

const promise2 = doSomething().then(successCallback, failureCallback);


This second promise (promise2) represents the completion not just of doSomething(), but also of the successCallback or failureCallback you passed in, which can be other asynchronous functions returning a promise. When that's the case, any callbacks added to promise2 get queued behind the promise returned by either successCallback or failureCallback.

promise2 不仅表示 doSomething() 函数的完成,也代表了你传入的 successCallback 或者 failureCallback 的完成,这两个函数也可以返回一个 Promise 对象,从而形成另一个异步操作,这样的话,在 promise2 上新增的回调函数会排在这个 Promise 对象的后面。


Basically, each promise represents the completion of another asynchronous step in the chain.
基本上,每一个 Promise 都代表了链中另一个异步过程的完成。

In the old days, doing several asynchronous operations in a row would lead to the classic callback pyramid of doom:
在过去,要想做多重的异步操作,会导致经典的回调地狱:

doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

 

With modern functions, we attach our callbacks to the returned promises instead, forming a promise chain:

现在,我们可以把回调绑定到返回的 Promise 上,形成一个 Promise 链:

doSomething()
.then(function(result) {
  return doSomethingElse(result);
})
.then(function(newResult) {
  return doThirdThing(newResult);
})
.then(function(finalResult) {
  console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);

 

The arguments to then are optional, and catch(failureCallback) is short for then(null, failureCallback). You might see this expressed with arrow functions instead:

then里的参数是可选的,catch(failureCallback) 是 then(null, failureCallback) 的缩略形式。如下所示,我们也可以用箭头函数来表示:

doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
  console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);

 

Important: Always return results, otherwise callbacks won't catch the result of a previous promise (with arrow functions () => x is short for () => { return x; }).

注意:一定要有返回值,否则,callback 将无法获取上一个 Promise 的结果。(如果使用箭头函数,() => x 比 () => { return x; } 更简洁一些,但后一种保留 return 的写法才支持使用多个语句。)

 

Chaining after a catch

Catch 的后续链式操作

 

It's possible to chain after a failure, i.e. a catch, which is useful to accomplish new actions even after an action failed in the chain. Read the following example:

有可能会在一个回调失败之后继续使用链式操作,即 使用一个 catch,这对于在链式操作中抛出一个失败之后,再次进行新的操作很有用。请阅读下面的例子:

new Promise((resolve, reject) => {
    console.log('Initial');

    resolve();
})
.then(() => {
    throw new Error('Something failed');
        
    console.log('Do this');
})
.catch(() => {
    console.error('Do that');
})
.then(() => {
    console.log('Do this, no matter what happened before');
});

 

This will output the following text:

输出结果如下:

Initial
Do that
Do this, no matter what happened before


Note: The text Do this is not displayed because the Something failed error caused a rejection.

注意:因为抛出了错误 Something failed,所以前一个 执行Do this 没有被输出。

 

Error propagation

错误传递


You might recall seeing failureCallback three times in the pyramid of doom earlier, compared to only once at the end of the promise chain:

在之前的回调地狱示例中,你可能记得有 3 次 failureCallback 的调用,而在 Promise 链中只有尾部的一次调用。

doSomething()
.then(result => doSomethingElse(value))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);


If there's an exception, the browser will look down the chain for .catch() handlers or onRejected. This is very much modeled after how synchronous code works:

通常,一遇到异常抛出,浏览器就会顺着promise链寻找下一个 onRejected 失败回调函数或者由 .catch() 指定的回调函数。这和以下的同步代码的执行过程很相似。

try {
  let result = syncDoSomething();
  let newResult = syncDoSomethingElse(result);
  let finalResult = syncDoThirdThing(newResult);
  console.log(`Got the final result: ${finalResult}`);
} catch(error) {
  failureCallback(error);
}

 

This symmetry with asynchronous code culminates in the async/await syntactic sugar in ECMAScript 2017:

在 ECMAScript 2017 标准的 async/await 语法糖中,这种与同步形式代码的对称性得到了极致的体现:

async function foo() {
  try {
    const result = await doSomething();
    const newResult = await doSomethingElse(result);
    const finalResult = await doThirdThing(newResult);
    console.log(`Got the final result: ${finalResult}`);
  } catch(error) {
    failureCallback(error);
  }
}

 

It builds on promises, e.g. doSomething() is the same function as before. You can read more about the syntax here.

这个例子是在 Promise 的基础上构建的,例如,doSomething() 与之前的函数是相同的。你可以在这里阅读更多的与此语法相关的文章。

Promises solve a fundamental flaw with the callback pyramid of doom, by catching all errors, even thrown exceptions and programming errors. This is essential for functional composition of asynchronous operations.

通过捕获所有的错误,甚至抛出异常和程序错误,Promise 解决了回调地狱的基本缺陷。这对于构建异步操作的基础功能而言是很有必要的。

 

Promise rejection events

Promise 拒绝事件


Whenever a promise is rejected, one of two events is sent to the global scope (generally, this is either the window or, if being used in a web worker, it's the Worker or other worker-based interface). The two events are:

当 Promise 被拒绝时,会有下文所述的两个事件之一被派发到全局作用域(通常而言,就是window;如果是在 web worker 中使用的话,就是 Worker 或者其他 worker-based 接口)。这两个事件如下所示:


rejectionhandled

Sent when a promise is rejected, after that rejection has been handled by the executor's reject function.

当 Promise 被拒绝、并且在 reject 函数处理该 rejection 之后会派发此事件。

 

unhandledrejection

Sent when a promise is rejected but there is no rejection handler available.

当 Promise 被拒绝,但没有提供 reject 函数来处理该 rejection 时,会派发此事件。

 

In both cases, the event (of type PromiseRejectionEvent) has as members a promise property indicating the promise that was rejected, and a reason property that provides the reason given for the promise to be rejected.

以上两种情况中,PromiseRejectionEvent 事件都有两个属性,一个是 promise 属性,该属性指向被驳回的 Promise,另一个是 reason 属性,该属性用来说明 Promise 被驳回的原因。

These make it possible to offer fallback error handling for promises, as well as to help debug issues with your promise management. These handlers are global per context, so all errors will go to the same event handlers, regardless of source.

因此,我们可以通过以上事件为 Promise 失败时提供补偿处理,也有利于调试 Promise 相关的问题。在每一个上下文中,该处理都是全局的,因此不管源码如何,所有的错误都会在同一个handler中被捕捉处理。


One case of special usefulness: when writing code for Node.js, it's common that modules you include in your project may have unhandled rejected promises. These get logged to the console by the Node runtime. You can capture these for analysis and handling by your code—or just to avoid having them cluttering up your output—by adding a handler for the unhandledrejection event, like this:

一个特别有用的例子:当你使用 Node.js 时,有些依赖模块可能会有未被处理的 rejected promises,这些都会在运行时打印到控制台。你可以在自己的代码中捕捉这些信息,然后添加与 unhandledrejection 相应的 handler 来做分析和处理,或只是为了让你的输出更整洁。举例如下:

window.addEventListener("unhandledrejection", event => {
  /* You might start here by adding code to examine the
     promise specified by event.promise and the reason in
     event.reason */
  /* 你可以在这里添加一些代码,以便检查
     event.promise 中的 promise 和
     event.reason 中的 rejection 原因 */

  event.preventDefault();
}, false);

 

 

By calling the event's preventDefault() method, you tell the JavaScript runtime not to do its default action when rejected promises go unhandled. That default action usually involves logging the error to console, and this is indeed the case for Node.

调用 event 的 preventDefault() 方法是为了告诉 JavaScript 引擎当 promise 被拒绝时不要执行默认操作,默认操作一般会包含把错误打印到控制台。

Ideally, of course, you should examine the rejected promises to make sure none of them are actual code bugs before just discarding these events.

理想情况下,在忽略这些事件之前,我们应该检查所有被拒绝的 Promise,来确认这不是代码中的 bug。

 

 

Creating a Promise around an old callback API

在旧式回调 API 中创建 Promise


A Promise can be created from scratch using its constructor. This should be needed only to wrap old APIs.

可以通过 Promise 的构造器从零开始创建 Promise。 这种方式(通过构造器的方式)应当只在包裹旧 API 的时候用到。


In an ideal world, all asynchronous functions would already return promises. Unfortunately, some APIs still expect success and/or failure callbacks to be passed in the old way. The most obvious example is the setTimeout() function:

理想状态下,所有的异步函数都已经返回 Promise 了。但有一些 API 仍然使用旧方式来传入的成功(或者失败)的回调。典型的例子就是 setTimeout() 函数:

setTimeout(() => saySomething("10 seconds passed"), 10000);

 


Mixing old-style callbacks and promises is problematic. If saySomething() fails or contains a programming error, nothing catches it. setTimeout is to blame for this.

混用旧式回调和 Promise 可能会造成运行时序问题。如果 saySomething 函数失败了,或者包含了编程错误,那就没有办法捕获它了。这得怪 setTimeout。


Luckily we can wrap setTimeout in a promise. Best practice is to wrap problematic functions at the lowest possible level, and then never call them directly again:

幸运地是,我们可以用 Promise 来包裹它。最好的做法是,将这些有问题的函数包装起来,留在底层,并且永远不要再直接调用它们:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);

 

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

console.log("start");
wait(3000).then(() => console.log("3 seconds")).catch();

 

Basically, the promise constructor takes an executor function that lets us resolve or reject a promise manually. Since setTimeout() doesn't really fail, we left out reject in this case.

通常,Promise 的构造器接收一个执行函数(executor),我们可以在这个执行函数里手动地 resolve 和 reject 一个 Promise。既然 setTimeout 并不会真的执行失败,那么我们可以在这种情况下忽略 reject。

 

Composition

组合


Promise.resolve() and Promise.reject() are shortcuts to manually create an already resolved or rejected promise respectively. This can be useful at times.

Promise.resolve() 和 Promise.reject() 是手动创建一个已经 resolve 或者 reject 的 Promise 快捷方法。它们有时很有用。


Promise.all() and Promise.race() are two composition tools for running asynchronous operations in parallel.

Promise.all() 和 Promise.race() 是并行运行异步操作的两个组合式工具。


We can start operations in parallel and wait for them all to finish like this:

我们可以发起并行操作,然后等多个操作全部结束后进行下一步操作,如下:

 

Promise.all([func1(), func2(), func3()])
.then(([result1, result2, result3]) => { /* use result1, result2 and result3 */ });

 

Sequential composition is possible using some clever JavaScript:
可以使用一些聪明的 JavaScript 写法实现时序组合:

[func1, func2, func3].reduce((p, f) => p.then(f), Promise.resolve())
.then(result3 => { /* use result3 */ });

 

const func1 = () => { console.log('func1'); return 'func1' };
const func2 = () => { console.log('func2'); return 'func2' };
const func3 = () => { console.log('func3'); return 'func3' };

[func1, func2, func3].reduce((p, f) => p.then(f), Promise.resolve())
    .then(result => { console.log(result) });

func1
func2
func3
func3

 

Basically, we reduce an array of asynchronous functions down to a promise chain equivalent to:
通常,我们递归调用一个由异步函数组成的数组时相当于一个 Promise 链:

Promise.resolve().then(func1).then(func2).then(func3);


This can be made into a reusable compose function, which is common in functional programming:

我们也可以写成可复用的函数形式,这在函数式编程中极为普遍:

const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));

 

The composeAsync() function will accept any number of functions as arguments, and will return a new function that accepts an initial value to be passed through the composition pipeline:

composeAsync() 函数将会接受任意数量的函数作为其参数,并返回一个新的函数,该函数接受一个通过 composition pipeline 传入的初始值。这对我们来说非常有益,因为任一函数可以是异步或同步的,它们能被保证按顺序执行:

const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(data);

 

const func1 = (x) => { return x + 10 };
const func2 = (x) => { return x + 100 };
const func3 = (x) => { console.log(x + 1000); return x + 1000 };

const applyAsync = (acc, val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));

const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(1);

1111

 

In ECMAScript 2017, sequential composition can be done more simply with async/await:

在 ECMAScript 2017 标准中, 时序组合可以通过使用 async/await 而变得更简单:

let result;
for (const f of [func1, func2, func3]) {
  result = await f(result);
}
/* use last result (i.e. result3) */

 

Timing

时序

To avoid surprises, functions passed to then() will never be called synchronously, even with an already-resolved promise:

为了避免意外,即使是一个已经变成 resolve 状态的 Promise,传递给 then() 的函数也总是会被异步调用:

Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2

 

Instead of running immediately, the passed-in function is put on a microtask queue, which means it runs later when the queue is emptied at the end of the current run of the JavaScript event loop, i.e. pretty soon:

传递到 then() 中的函数被置入了一个微任务队列,而不是立即执行,这意味着它是在 JavaScript 事件队列的所有运行时结束了,事件队列被清空之后,才开始执行:

const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait().then(() => console.log(4));
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1); // 1, 2, 3, 4

 

Nesting

嵌套


Simple promise chains are best kept flat without nesting, as nesting can be a result of careless composition. See common mistakes.

简便的 Promise 链式编程最好保持扁平化,不要嵌套 Promise,因为嵌套经常会是粗心导致的。可查阅下一节的常见错误中的例子。

Nesting is a control structure to limit the scope of catch statements. Specifically, a nested catch only catches failures in its scope and below, not errors higher up in the chain outside the nested scope. When used correctly, this gives greater precision in error recovery:

嵌套 Promise 是一种可以限制 catch 语句的作用域的控制结构写法。明确来说,嵌套的 catch 仅捕捉在其之前同时还必须是其作用域的 failureres,而捕捉不到在其链式以外或者其嵌套域以外的 error。如果使用正确,那么可以实现高精度的错误修复。

doSomethingCritical()
.then(result => doSomethingOptional(result)
  .then(optionalResult => doSomethingExtraNice(optionalResult))
  .catch(e => {})) // Ignore if optional stuff fails; proceed.
.then(() => moreCriticalStuff())
.catch(e => console.error("Critical failure: " + e.message));

 

Note that the optional steps here are nested, not from the indentation, but from the precarious placement of the outer ( and ) around them.

注意,有些代码步骤是嵌套的,而不是一个简单的纯链式,这些语句前与后都被括号 () 包裹着。

The inner neutralizing catch statement only catches failures from doSomethingOptional() and doSomethingExtraNice(), after which the code resumes with moreCriticalStuff(). Importantly, if doSomethingCritical() fails, its error is caught by the final (outer) catch only.

这个内部的 catch 语句仅能捕获到 doSomethingOptional() 和 doSomethingExtraNice() 的失败,之后就恢复到moreCriticalStuff() 的运行。重要提醒:如果 doSomethingCritical() 失败,这个错误仅会被最后的(外部)catch 语句捕获到。

 

Common mistakes

常见错误

Here are some common mistakes to watch out for when composing promise chains. Several of these mistakes manifest in the following example:
在编写 Promise 链时,需要注意以下示例中展示的几个错误:

// Bad example! Spot 3 mistakes!

doSomething().then(function(result) {
  doSomethingElse(result) // Forgot to return promise from inner chain + unnecessary nesting
  .then(newResult => doThirdThing(newResult));
}).then(() => doFourthThing());
// Forgot to terminate chain with a catch!

 

The first mistake is to not chain things together properly. This happens when we create a new promise but forget to return it. As a consequence, the chain is broken, or rather, we have two independent chains racing. This means doFourthThing() won't wait for doSomethingElse() or doThirdThing() to finish, and will run in parallel with them, likely unintended. Separate chains also have separate error handling, leading to uncaught errors.

第一个错误是没有正确地将事物相连接。当我们创建新 Promise 但忘记返回它时,会发生这种情况。因此,链条被打破,或者更确切地说,我们有两个独立的链条竞争(同时在执行两个异步而非一个一个的执行)。这意味着 doFourthThing() 不会等待 doSomethingElse() 或 doThirdThing() 完成,并且将与它们并行运行,可能是无意的。单独的链也有单独的错误处理,导致未捕获的错误。


The second mistake is to nest unnecessarily, enabling the first mistake. Nesting also limits the scope of inner error handlers, which—if unintended—can lead to uncaught errors. A variant of this is the promise constructor anti-pattern, which combines nesting with redundant use of the promise constructor to wrap code that already uses promises.

第二个错误是不必要地嵌套,实现第一个错误。嵌套还限制了内部错误处理程序的范围,如果是非预期的,可能会导致未捕获的错误。其中一个变体是 Promise 构造函数反模式,它结合了 Promise 构造函数的多余使用和嵌套。

 

The third mistake is forgetting to terminate chains with catch. Unterminated promise chains lead to uncaught promise rejections in most browsers.

第三个错误是忘记用 catch 终止链。这导致在大多数浏览器中不能终止的 Promise 链里的 rejection。

 

A good rule-of-thumb is to always either return or terminate promise chains, and as soon as you get a new promise, return it immediately, to flatten things:

一个好的经验法则是总是返回或终止 Promise 链,并且一旦你得到一个新的 Promise,返回它。下面是修改后的平面化的代码:

doSomething()
.then(function(result) {
  return doSomethingElse(result);
})
.then(newResult => doThirdThing(newResult))
.then(() => doFourthThing())
.catch(error => console.error(error));

 

Note that () => x is short for () => { return x; }.

注意:() => x 是 () => { return x; } 的简写。

Now we have a single deterministic chain with proper error handling.

上述代码的写法就是具有适当错误处理的简单明确的链式写法。

Using async/await addresses most, if not all of these problems—the tradeoff being that the most common mistake with that syntax is forgetting the await keyword.

使用 async/await 可以解决以上大多数错误,使用 async/await 时,最常见的错误就是忘记了 await 关键字。

 

When promises and tasks collide

If you run into situations in which you have promises and tasks (such as events or callbacks) which are firing in unpredictable orders, it's possible you may benefit from using a microtask to check status or balance out your promises when promises are created conditionally.

If you think microtasks may help solve this problem, see the microtask guide to learn more about how to use queueMicrotask() to enqueue a function as a microtask.

 

posted on 2020-04-08 00:26  Mr.Winter  阅读(251)  评论(0编辑  收藏  举报