Promise

基线 广泛可用

此功能已得到充分确立,可在许多设备和浏览器版本上运行。它自以下时间起在所有浏览器中可用 2015 年 7 月.

Promise 对象表示异步操作最终完成(或失败)及其结果值。

要了解 Promise 的工作原理以及如何使用它们,我们建议您先阅读使用 Promise

描述

Promise 是尚未确定的值的代理。它允许您将处理程序与异步操作的最终成功值或失败原因相关联。这使异步方法能够像同步方法一样返回值:异步方法不会立即返回最终值,而是返回一个Promise,承诺在将来的某个时间点提供该值。

Promise 处于以下三种状态之一:

  • 挂起:初始状态,既未完成也未拒绝。
  • 完成:表示操作已成功完成。
  • 拒绝:表示操作失败。

挂起 Promise 的最终状态可以是完成(包含一个值)或拒绝(包含一个原因(错误))。当其中任一选项发生时,Promise 的then 方法排队的相关处理程序将被调用。如果 Promise 在附加相应的处理程序时已完成或拒绝,该处理程序将被调用,因此异步操作完成与处理程序附加之间不会出现竞争条件。

如果 Promise 完成或拒绝,但未挂起,则该 Promise 被称为已解决

Flowchart showing how the Promise state transitions between pending, fulfilled, and rejected via then/catch handlers. A pending promise can become either fulfilled or rejected. If fulfilled, the "on fulfillment" handler, or first parameter of the then() method, is executed and carries out further asynchronous actions. If rejected, the error handler, either passed as the second parameter of the then() method or as the sole parameter of the catch() method, gets executed.

您还会听到与 Promise 相关的已解析一词——这意味着 Promise 已解决或“锁定”以匹配另一个 Promise 的最终状态,并且进一步解析或拒绝它不会产生任何影响。原始 Promise 提案中的状态和命运文档包含有关 Promise 术语的更多详细信息。口头上,"已解析" Promise 通常等同于 "已完成" Promise,但正如 "状态和命运" 中所示,已解析 Promise 也可以是挂起或拒绝的。例如

js
new Promise((resolveOuter) => {
  resolveOuter(
    new Promise((resolveInner) => {
      setTimeout(resolveInner, 1000);
    }),
  );
});

此 Promise 在创建时就已经已解析(因为resolveOuter 是同步调用的),但它是用另一个 Promise 解析的,因此直到 1 秒后内层 Promise 完成时才会完成。在实际应用中,“解析”通常在后台完成,不可观察,只有其完成或拒绝才是可观察的。

注意:其他几种语言也拥有延迟计算或懒惰求值机制,它们也将这种机制称为“Promise”,例如 Scheme。JavaScript 中的 Promise 代表正在发生的进程,可以使用回调函数将这些进程链接起来。如果您希望懒惰求值表达式,请考虑使用没有参数的函数(例如f = () => expression)来创建懒惰求值的表达式,并使用f() 来立即求值表达式。

Promise 本身没有用于取消的标准协议,但您可能能够直接取消底层异步操作,通常使用AbortController

链式 Promise

Promise 方法then()catch()finally()用于将进一步的操作与已解决的 Promise 关联。then() 方法最多接受两个参数;第一个参数是 Promise 完成情况下的回调函数,第二个参数是 Promise 拒绝情况下的回调函数。catch()finally() 方法在内部调用then() 并使错误处理更简洁。例如,catch() 实际上只是没有传递完成处理程序的then()。由于这些方法会返回 Promise,因此可以将它们链接起来。例如

js
const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("foo");
  }, 300);
});

myPromise
  .then(handleFulfilledA, handleRejectedA)
  .then(handleFulfilledB, handleRejectedB)
  .then(handleFulfilledC, handleRejectedC);

我们将使用以下术语:初始 Promise 是调用then 的 Promise;新 Promisethen 返回的 Promise。传递给then 的两个回调函数分别称为完成处理程序拒绝处理程序

初始 Promise 的已解决状态决定执行哪个处理程序。

  • 如果初始 Promise 完成,则完成处理程序将使用完成值调用。
  • 如果初始 Promise 拒绝,则拒绝处理程序将使用拒绝原因调用。

处理程序函数的完成决定新 Promise 的已解决状态。

  • 如果处理程序函数返回Thenable 值,则新 Promise 将与返回的 Promise 处于相同状态。
  • 如果处理程序函数返回非 Thenable 值,则新 Promise 将使用返回的值完成。
  • 如果处理程序函数引发错误,则新 Promise 将使用引发的错误拒绝。
  • 如果初始 Promise 没有附加相应的处理程序,则新 Promise 将与初始 Promise 处于相同状态——也就是说,没有拒绝处理程序的情况下,拒绝的 Promise 将保持拒绝状态,并具有相同的拒绝原因。

例如,在上面的代码中,如果myPromise 拒绝,handleRejectedA 将被调用,如果handleRejectedA 正常完成(不引发或返回拒绝的 Promise),则第一个then 返回的 Promise 将完成,而不是保持拒绝状态。因此,如果必须立即处理错误,但我们希望将错误状态沿链传递,则必须在拒绝处理程序中引发某种类型的错误。另一方面,如果不需要立即处理错误,则在最终catch() 处理程序之前省略错误处理更简单。

js
myPromise
  .then(handleFulfilledA)
  .then(handleFulfilledB)
  .then(handleFulfilledC)
  .catch(handleRejectedAny);

使用箭头函数 作为回调函数,Promise 链的实现可能如下所示

js
myPromise
  .then((value) => `${value} and bar`)
  .then((value) => `${value} and bar again`)
  .then((value) => `${value} and again`)
  .then((value) => `${value} and again`)
  .then((value) => {
    console.log(value);
  })
  .catch((err) => {
    console.error(err);
  });

注意:为了更快地执行,所有同步操作最好在一个处理程序内完成,否则执行所有处理程序的顺序将需要多个时钟周期。

JavaScript 保持一个作业队列。每次,JavaScript 从队列中挑选一个作业并将其执行到完成。这些作业由Promise() 构造函数的执行程序、传递给then 的处理程序或返回 Promise 的任何平台 API 定义。链中的 Promise 表示这些作业之间的依赖关系。当 Promise 解决时,与其关联的相应处理程序将被添加到作业队列的末尾。

一个 Promise 可以参与多个链。对于以下代码,promiseA 的完成将导致handleFulfilled1handleFulfilled2 都被添加到作业队列。由于handleFulfilled1 是先注册的,因此它将先被调用。

js
const promiseA = new Promise(myExecutorFunc);
const promiseB = promiseA.then(handleFulfilled1, handleRejected1);
const promiseC = promiseA.then(handleFulfilled2, handleRejected2);

可以将动作分配给已经解决的承诺。在这种情况下,动作将立即添加到作业队列的末尾,并在所有现有作业完成后执行。因此,针对已“解决”的承诺的动作将仅在当前同步代码完成后且至少经过一个循环刻度后发生。这保证了承诺动作是异步的。

js
const promiseA = new Promise((resolve, reject) => {
  resolve(777);
});
// At this point, "promiseA" is already settled.
promiseA.then((val) => console.log("asynchronous logging has val:", val));
console.log("immediate logging");

// produces output in this order:
// immediate logging
// asynchronous logging has val: 777

Thenable

在 JavaScript 生态系统中,在 Promise 成为语言的一部分之前,已经有了多个 Promise 实现。尽管内部表示不同,但至少所有类似 Promise 的对象都实现了 *Thenable* 接口。Thenable 实现 .then() 方法,该方法使用两个回调调用:一个用于承诺已完成时,另一个用于承诺被拒绝时。承诺也是 Thenable。

为了与现有的 Promise 实现互操作,语言允许使用 Thenable 来代替承诺。例如,Promise.resolve 不仅可以解决承诺,还可以追踪 Thenable。

js
const aThenable = {
  then(onFulfilled, onRejected) {
    onFulfilled({
      // The thenable is fulfilled with another thenable
      then(onFulfilled, onRejected) {
        onFulfilled(42);
      },
    });
  },
};

Promise.resolve(aThenable); // A promise fulfilled with 42

承诺并发

Promise 类提供四种静态方法来促进异步任务 并发

Promise.all()

所有承诺都完成时完成;当任何承诺被拒绝时拒绝。

Promise.allSettled()

所有承诺都解决时完成。

Promise.any()

任何承诺完成时完成;当所有承诺被拒绝时拒绝。

Promise.race()

任何承诺解决时解决。换句话说,当任何承诺完成时完成;当任何承诺被拒绝时拒绝。

所有这些方法都接受一个承诺的 可迭代 对象(确切地说是 Thenable)并返回一个新的承诺。它们都支持子类化,这意味着它们可以被调用在 Promise 的子类上,结果将是一个子类类型的承诺。为此,子类的构造函数必须实现与 Promise() 构造函数相同的签名——接受一个单一的 executor 函数,该函数可以使用 resolvereject 回调作为参数进行调用。子类还必须有一个 resolve 静态方法,可以像 Promise.resolve() 一样调用,以将值解析为承诺。

请注意,JavaScript 本质上是 单线程 的,因此在任何给定的时刻,只有一个任务正在执行,尽管控制可以在不同的承诺之间切换,使承诺的执行看起来是并发的。JavaScript 中的 并行执行 只能通过 工作线程 实现。

构造函数

Promise()

创建一个新的 Promise 对象。构造函数主要用于包装尚不支持承诺的函数。

静态属性

Promise[Symbol.species]

返回用于构建承诺方法返回值的构造函数。

静态方法

Promise.all()

以承诺的可迭代对象作为输入,并返回单个 Promise。当所有输入承诺都完成时(包括传递空可迭代对象时),此返回的承诺完成,并包含完成值的数组。当任何输入承诺被拒绝时,它会拒绝,并包含第一个拒绝原因。

Promise.allSettled()

以承诺的可迭代对象作为输入,并返回单个 Promise。当所有输入承诺都解决时(包括传递空可迭代对象时),此返回的承诺完成,并包含描述每个承诺结果的对象数组。

Promise.any()

以承诺的可迭代对象作为输入,并返回单个 Promise。当任何输入承诺完成时,此返回的承诺完成,并包含第一个完成值。当所有输入承诺都拒绝时(包括传递空可迭代对象时),它会拒绝,并包含一个 AggregateError,其中包含一个拒绝原因数组。

Promise.race()

以承诺的可迭代对象作为输入,并返回单个 Promise。此返回的承诺使用第一个解决的承诺的最终状态解决。

Promise.reject()

返回一个新的 Promise 对象,该对象使用给定的原因被拒绝。

Promise.resolve()

返回一个使用给定值解决的 Promise 对象。如果该值是 Thenable(即具有 then 方法),则返回的承诺将“跟随”该 Thenable,并采用其最终状态;否则,返回的承诺将使用该值完成。

Promise.try() 实验性

接受任何类型的回调(返回或抛出,同步或异步)并将结果包装在 Promise 中。

Promise.withResolvers()

返回一个包含新的 Promise 对象和两个函数的对象,用于解决或拒绝它,分别对应于传递给 Promise() 构造函数的执行器的两个参数。

实例属性

这些属性在 Promise.prototype 上定义,并由所有 Promise 实例共享。

Promise.prototype.constructor

创建实例对象的构造函数。对于 Promise 实例,初始值为 Promise 构造函数。

Promise.prototype[Symbol.toStringTag]

[Symbol.toStringTag] 属性的初始值为字符串 "Promise"。此属性在 Object.prototype.toString() 中使用。

实例方法

Promise.prototype.catch()

将拒绝处理程序回调附加到承诺,并返回一个新的承诺,该承诺解析为回调的返回值(如果调用),或解析为其原始完成值(如果承诺改为完成)。

Promise.prototype.finally()

将处理程序附加到承诺,并返回一个新的承诺,该承诺在原始承诺解决时解决。当承诺解决时,处理程序被调用,无论是完成还是拒绝。

Promise.prototype.then()

将完成处理程序和拒绝处理程序附加到承诺,并返回一个新的承诺,该承诺解析为调用处理程序的返回值,或解析为其原始解决值(如果承诺未被处理(即,如果相关处理程序 onFulfilledonRejected 不是函数))。

示例

基本示例

js
const myFirstPromise = new Promise((resolve, reject) => {
  // We call resolve(...) when what we were doing asynchronously was successful, and reject(...) when it failed.
  // In this example, we use setTimeout(...) to simulate async code.
  // In reality, you will probably be using something like XHR or an HTML API.
  setTimeout(() => {
    resolve("Success!"); // Yay! Everything went well!
  }, 250);
});

myFirstPromise.then((successMessage) => {
  // successMessage is whatever we passed in the resolve(...) function above.
  // It doesn't have to be a string, but if it is only a succeed message, it probably will be.
  console.log(`Yay! ${successMessage}`);
});

具有不同情况的示例

此示例展示了使用 Promise 功能和可能发生的各种情况的不同技术。要理解这一点,请从代码块的底部开始滚动,并检查承诺链。在提供初始承诺后,可以跟随一系列承诺。该链由 .then() 调用组成,通常(但并非必须)在末尾有一个 .catch(),可以选择地后跟 .finally()。在此示例中,承诺链由自定义编写的 new Promise() 构造启动;但在实际实践中,承诺链更常见地从返回承诺的 API 函数(由其他人编写)开始。

示例函数 tetheredGetNumber() 显示承诺生成器将在设置异步调用时或在回调中,或两者都使用 reject()。函数 promiseGetWord() 说明了 API 函数如何以自包含的方式生成和返回承诺。

请注意,函数 troubleWithGetNumber()throw 结束。这是强制执行的,因为承诺链会遍历所有 .then() 承诺,即使在错误发生之后也是如此,如果没有 throw,错误将看起来“已修复”。这很麻烦,因此,通常在 .then() 承诺链中省略 onRejected,只在最终的 catch() 中有一个 onRejected

此代码可以在 NodeJS 下运行。看到实际发生的错误可以增强理解。要强制更多错误,请更改 threshold 值。

js
// To experiment with error handling, "threshold" values cause errors randomly
const THRESHOLD_A = 8; // can use zero 0 to guarantee error

function tetheredGetNumber(resolve, reject) {
  setTimeout(() => {
    const randomInt = Date.now();
    const value = randomInt % 10;
    if (value < THRESHOLD_A) {
      resolve(value);
    } else {
      reject(`Too large: ${value}`);
    }
  }, 500);
}

function determineParity(value) {
  const isOdd = value % 2 === 1;
  return { value, isOdd };
}

function troubleWithGetNumber(reason) {
  const err = new Error("Trouble getting number", { cause: reason });
  console.error(err);
  throw err;
}

function promiseGetWord(parityInfo) {
  return new Promise((resolve, reject) => {
    const { value, isOdd } = parityInfo;
    if (value >= THRESHOLD_A - 1) {
      reject(`Still too large: ${value}`);
    } else {
      parityInfo.wordEvenOdd = isOdd ? "odd" : "even";
      resolve(parityInfo);
    }
  });
}

new Promise(tetheredGetNumber)
  .then(determineParity, troubleWithGetNumber)
  .then(promiseGetWord)
  .then((info) => {
    console.log(`Got: ${info.value}, ${info.wordEvenOdd}`);
    return info;
  })
  .catch((reason) => {
    if (reason.cause) {
      console.error("Had previously handled error");
    } else {
      console.error(`Trouble with promiseGetWord(): ${reason}`);
    }
  })
  .finally((info) => console.log("All done"));

高级示例

这个小示例展示了 Promise 的机制。每次单击 <button> 时,都会调用 testPromise() 方法。它创建一个承诺,该承诺将使用 setTimeout() 在 1-3 秒(随机)内完成,并使用承诺计数(从 1 开始的数字)。Promise() 构造函数用于创建承诺。

承诺的完成将通过使用 p1.then() 设置的完成回调进行记录。一些日志显示了方法的同步部分如何与承诺的异步完成分离。

通过在短时间内多次单击按钮,您甚至会看到不同的承诺一个接一个地完成。

HTML

html
<button id="make-promise">Make a promise!</button>
<div id="log"></div>

JavaScript

js
"use strict";

let promiseCount = 0;

function testPromise() {
  const thisPromiseCount = ++promiseCount;
  const log = document.getElementById("log");
  // begin
  log.insertAdjacentHTML("beforeend", `${thisPromiseCount}) Started<br>`);
  // We make a new promise: we promise a numeric count of this promise,
  // starting from 1 (after waiting 3s)
  const p1 = new Promise((resolve, reject) => {
    // The executor function is called with the ability
    // to resolve or reject the promise
    log.insertAdjacentHTML(
      "beforeend",
      `${thisPromiseCount}) Promise constructor<br>`,
    );
    // This is only an example to create asynchronism
    setTimeout(
      () => {
        // We fulfill the promise
        resolve(thisPromiseCount);
      },
      Math.random() * 2000 + 1000,
    );
  });

  // We define what to do when the promise is resolved with the then() call,
  // and what to do when the promise is rejected with the catch() call
  p1.then((val) => {
    // Log the fulfillment value
    log.insertAdjacentHTML("beforeend", `${val}) Promise fulfilled<br>`);
  }).catch((reason) => {
    // Log the rejection reason
    console.log(`Handle rejected promise (${reason}) here.`);
  });
  // end
  log.insertAdjacentHTML("beforeend", `${thisPromiseCount}) Promise made<br>`);
}

const btn = document.getElementById("make-promise");
btn.addEventListener("click", testPromise);

结果

使用 XHR 加载图像

另一个使用 PromiseXMLHttpRequest 加载图像的简单示例在 MDN GitHub js-examples 存储库中提供。您还可以 查看它的实际运行情况。每个步骤都有注释,可以让您密切关注 Promise 和 XHR 架构。

在任职设置对象跟踪

设置对象是一个 环境,在运行 JavaScript 代码时提供更多信息。这包括领域和模块映射,以及 HTML 特定的信息,例如源代码。跟踪在任职设置对象,以确保浏览器知道为给定的用户代码使用哪个设置对象。

为了更好地理解这一点,我们可以更仔细地看看领域可能带来的问题。领域 可以粗略地理解为全局对象。领域独一无二之处在于它们保存了运行 JavaScript 代码所需的所有信息。这包括诸如 ArrayError 之类的对象。每个设置对象都有自己的这些对象的“副本”,它们不共享。这可能会导致与承诺相关的某些意外行为。为了解决这个问题,我们跟踪了一种称为 在任职设置对象 的东西。它表示与导致某个函数调用的用户代码上下文相关的特定信息。

为了进一步说明这一点,我们可以看看嵌入在文档中的 <iframe> 如何与其宿主通信。由于所有 Web API 都知道在任职设置对象,因此以下内容将在所有浏览器中运行

html
<!doctype html> <iframe></iframe>
<!-- we have a realm here -->
<script>
  // we have a realm here as well
  const bound = frames[0].postMessage.bind(frames[0], "some data", "*");
  // bound is a built-in function — there is no user
  // code on the stack, so which realm do we use?
  setTimeout(bound);
  // this still works, because we use the youngest
  // realm (the incumbent) on the stack
</script>

如果我们稍微修改上面的示例,就会得到以下结果

html
<!doctype html> <iframe></iframe>
<!-- we have a realm here -->
<script>
  // we have a realm here as well
  const bound = frames[0].postMessage.bind(frames[0], "some data", "*");
  // bound is a built in function — there is no user
  // code on the stack — which realm do we use?
  Promise.resolve(undefined).then(bound);
  // this still works, because we use the youngest
  // realm (the incumbent) on the stack
</script>

如果我们更改它,使文档中的 <iframe> 侦听发布的消息,我们可以观察到在任职设置对象的影响

html
<!-- y.html -->
<!doctype html>
<iframe src="x.html"></iframe>
<script>
  const bound = frames[0].postMessage.bind(frames[0], "some data", "*");
  Promise.resolve(undefined).then(bound);
</script>
html
<!-- x.html -->
<!doctype html>
<script>
  window.addEventListener(
    "message",
    (event) => {
      document.querySelector("#text").textContent = "hello";
      // this code will only run in browsers that track the incumbent settings object
      console.log(event);
    },
    false,
  );
</script>

在上面的示例中,<iframe> 的内部文本只有在跟踪在任职设置对象时才会更新。这是因为,如果没有跟踪在任职设置对象,我们最终可能会使用错误的环境来发送消息。

注意:目前,在任职领域跟踪在 Firefox 中已完全实现,在 Chrome 和 Safari 中已部分实现。

规范

规范
ECMAScript 语言规范
# sec-promise-objects

浏览器兼容性

BCD 表格仅在启用 JavaScript 的浏览器中加载。

另请参阅