使用 promise

Promise 是表示异步操作最终完成或失败的对象。由于大多数人都是已创建的 Promise 的消费者,因此本指南将在解释如何创建返回的 Promise 之前解释它们的消费。

¥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 是一个返回的对象,你可以将回调附加到该对象,而不是将回调传递到函数中。想象一个函数 createAudioFileAsync(),它在给定配置记录和两个回调函数的情况下异步生成声音文件:如果音频文件成功创建,则调用一个函数,如果发生错误,则调用另一个函数。

¥Essentially, a promise is a returned object to which you attach callbacks, instead of passing callbacks into a function. 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():

js
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);

如果 createAudioFileAsync() 被重写以返回承诺,则你可以将回调附加到它:

¥If createAudioFileAsync() were rewritten to return a promise, you would attach your callbacks to it instead:

js
createAudioFileAsync(audioSettings).then(successCallback, failureCallback);

该约定有几个优点。我们将逐一探讨。

¥This convention has several advantages. We will explore each one.

链接

¥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. In the old days, doing several asynchronous operations in a row would lead to the classic callback hell:

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

通过承诺,我们通过创建承诺链来实现这一目标。Promise 的 API 设计使得这一切变得很棒,因为回调被附加到返回的 Promise 对象,而不是传递到函数中。

¥With promises, we accomplish this by creating a promise chain. The API design of promises makes this great, because callbacks are attached to the returned promise object, instead of being passed into a function.

这就是魔法:then() 函数返回一个新的 Promise,与原来的不同:

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

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

第二个 Promise (promise2) 不仅代表 doSomething() 的完成,还代表你传入的 successCallbackfailureCallback 的完成 - 它们可以是返回 Promise 的其他异步函数。在这种情况下,添加到 promise2 的任何回调都会在 successCallbackfailureCallback 返回的 Promise 后面排队。

¥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.

注意:如果你想要一个可以使用的示例,你可以使用以下模板来创建任何返回 Promise 的函数:

¥Note: If you want a working example to play with, you can use the following template to create any function returning a promise:

js
function doSomething() {
  return new Promise((resolve) => {
    setTimeout(() => {
      // Other things to do before completion of the promise
      console.log("Did something");
      // The fulfillment value of the promise
      resolve("https://example.com/");
    }, 200);
  });
}

下面的 围绕旧回调 API 创建 Promise 部分讨论了该实现。

¥The implementation is discussed in the Creating a Promise around an old callback API section below.

使用此模式,你可以创建更长的处理链,其中每个 Promise 代表链中一个异步步骤的完成。此外,then 的参数是可选的,catch(failureCallback)then(null, failureCallback) 的缩写 - 因此,如果所有步骤的错误处理代码都相同,则可以将其附加到链的末尾:

¥With this pattern, you can create longer chains of processing, where each promise represents the completion of one asynchronous step in the chain. In addition, the arguments to then are optional, and catch(failureCallback) is short for then(null, failureCallback) — so if your error handling code is the same for all steps, you can attach it to the end of the chain:

js
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);

你可能会看到用 箭头函数 来表示:

¥You might see this expressed with arrow functions instead:

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

注意:箭头函数表达式可以有 隐式返回;所以,() => x() => { return x; } 的缩写。

¥Note: Arrow function expressions can have an implicit return; so, () => x is short for () => { return x; }.

doSomethingElsedoThirdThing 可以返回任何值 - 如果它们返回 Promise,则首先等待该 Promise 直到它解决,然后下一个回调接收履行值,而不是 Promise 本身。始终从 then 回调返回 Promise 非常重要,即使 Promise 始终解析为 undefined。如果前一个处理程序启动了一个 Promise 但没有返回它,则无法再跟踪其结算,并且该 Promise 被称为 "floating"。

¥doSomethingElse and doThirdThing can return any value — if they return promises, that promise is first waited until it settles, and the next callback receives the fulfillment value, not the promise itself. It is important to always return promises from then callbacks, even if the promise always resolves to undefined. If the previous handler started a promise but did not return it, there's no way to track its settlement anymore, and the promise is said to be "floating".

js
doSomething()
  .then((url) => {
    // Missing `return` keyword in front of fetch(url).
    fetch(url);
  })
  .then((result) => {
    // result is undefined, because nothing is returned from the previous
    // handler. There's no way to know the return value of the fetch()
    // call anymore, or whether it succeeded at all.
  });

通过返回 fetch 调用的结果(这是一个承诺),我们可以跟踪其完成情况并在完成时接收其值。

¥By returning the result of the fetch call (which is a promise), we can both track its completion and receive its value when it completes.

js
doSomething()
  .then((url) => {
    // `return` keyword added
    return fetch(url);
  })
  .then((result) => {
    // result is a Response object
  });

如果存在竞争条件,浮动承诺可能会更糟 - 如果未返回最后一个处理程序的承诺,则下一个 then 处理程序将被提前调用,并且它读取的任何值都可能不完整。

¥Floating promises could be worse if you have race conditions — if the promise from the last handler is not returned, the next then handler will be called early, and any value it reads may be incomplete.

js
const listOfIngredients = [];

doSomething()
  .then((url) => {
    // Missing `return` keyword in front of fetch(url).
    fetch(url)
      .then((res) => res.json())
      .then((data) => {
        listOfIngredients.push(data);
      });
  })
  .then(() => {
    console.log(listOfIngredients);
    // listOfIngredients will always be [], because the fetch request hasn't completed yet.
  });

因此,根据经验,只要你的操作遇到 Promise,就返回它并将其处理推迟到下一个 then 处理程序。

¥Therefore, as a rule of thumb, whenever your operation encounters a promise, return it and defer its handling to the next then handler.

js
const listOfIngredients = [];

doSomething()
  .then((url) => {
    // `return` keyword now included in front of fetch call.
    return fetch(url)
      .then((res) => res.json())
      .then((data) => {
        listOfIngredients.push(data);
      });
  })
  .then(() => {
    console.log(listOfIngredients);
    // listOfIngredients will now contain data from fetch call.
  });

更好的是,你可以将嵌套链展平为单个链,这更简单并且使错误处理更容易。详细信息将在下面的 嵌套 部分中讨论。

¥Even better, you can flatten the nested chain into a single chain, which is simpler and makes error handling easier. The details are discussed in the Nesting section below.

js
doSomething()
  .then((url) => fetch(url))
  .then((res) => res.json())
  .then((data) => {
    listOfIngredients.push(data);
  })
  .then(() => {
    console.log(listOfIngredients);
  });

使用 async/await 可以帮助你编写更直观且类似于同步代码的代码。下面是使用 async/await 的相同示例:

¥Using async/await can help you write code that's more intuitive and resembles synchronous code. Below is the same example using async/await:

js
async function logIngredients() {
  const url = await doSomething();
  const res = await fetch(url);
  const data = await res.json();
  listOfIngredients.push(data);
  console.log(listOfIngredients);
}

请注意,除了 Promise 前面的 await 关键字之外,代码看起来与同步代码完全相同。唯一的权衡之一是可能很容易忘记 await 关键字,该关键字只有在类型不匹配时才能修复(例如尝试使用 Promise 作为值)。

¥Note how the code looks exactly like synchronous code, except for the await keywords in front of promises. One of the only tradeoffs is that it may be easy to forget the await keyword, which can only be fixed when there's a type mismatch (e.g. trying to use a promise as a value).

async/await 建立在 Promise 的基础上 - 例如,doSomething() 与以前的功能相同,因此从 Promise 更改为 async/await 需要进行最少的重构。你可以在 异步函数await 参考文献中阅读有关 async/await 语法的更多信息。

¥async/await builds on promises — for example, doSomething() is the same function as before, so there's minimal refactoring needed to change from promises to async/await. You can read more about the async/await syntax in the async functions and await references.

注意:async/await 与普通的 Promise 链具有相同的并发语义。一个异步函数中的 await 不会停止整个程序,只会停止依赖于其值的部分,因此在 await 挂起时其他异步作业仍然可以运行。

¥Note: async/await has the same concurrency semantics as normal promise chains. await within one async function does not stop the entire program, only the parts that depend on its value, so other async jobs can still run while the await is pending.

错误处理

¥Error handling

你可能还记得早些时候在末日金字塔中见过 failureCallback 三次,而在承诺链末端只见过一次:

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

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

如果出现异常,浏览器将在链中查找 .catch() 处理程序或 onRejected。这在很大程度上模仿了同步代码的工作原理:

¥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:

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

这种与异步代码的对称性在 async/await 语法中达到了顶峰:

¥This symmetry with asynchronous code culminates in the async/await syntax:

js
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);
  }
}

Promise 通过捕获所有错误,甚至抛出的异常和编程错误,解决了厄运回调金字塔的基本缺陷。这对于异步操作的功能组合至关重要。所有错误现在都由链末尾的 catch() 方法处理,并且你几乎永远不需要在不使用 async/await 的情况下使用 try/catch

¥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. All errors are now handled by the catch() method at the end of the chain, and you should almost never need to use try/catch without using async/await.

嵌套

¥Nesting

在上面涉及 listOfIngredients 的示例中,第一个示例将一个 Promise 链嵌套在另一个 then() 处理程序的返回值中,而第二个示例则使用完全扁平的链。简单的承诺链最好保持平坦而不嵌套,因为嵌套可能是不小心组合的结果。

¥In the examples above involving listOfIngredients, the first one has one promise chain nested in the return value of another then() handler, while the second one uses an entirely flat chain. Simple promise chains are best kept flat without nesting, as nesting can be a result of careless composition.

嵌套是一种限制 catch 语句范围的控制结构。具体来说,嵌套 catch 仅捕获其范围及以下范围内的故障,而不捕获嵌套范围之外的链中更高的错误。如果正确使用,这可以提高错误恢复的精度:

¥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:

js
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 — with the nesting caused not by the indentation, but by the placement of the outer ( and ) parentheses around the steps.

内部错误沉默 catch 处理程序仅捕获来自 doSomethingOptional()doSomethingExtraNice() 的故障,之后代码将使用 moreCriticalStuff() 恢复。重要的是,如果 doSomethingCritical() 失败,其错误仅由最终(外部)catch 捕获,并且不会被内部 catch 处理程序吞没。

¥The inner error-silencing catch handler 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, and does not get swallowed by the inner catch handler.

async/await 中,此代码如下所示:

¥In async/await, this code looks like:

js
async function main() {
  try {
    const result = await doSomethingCritical();
    try {
      const optionalResult = await doSomethingOptional(result);
      await doSomethingExtraNice(optionalResult);
    } catch (e) {
      // Ignore failures in optional steps and proceed.
    }
    await moreCriticalStuff();
  } catch (e) {
    console.error(`Critical failure: ${e.message}`);
  }
}

注意:如果你没有复杂的错误处理,你很可能不需要嵌套的 then 处理程序。相反,使用扁平链并将错误处理逻辑放在末尾。

¥Note: If you don't have sophisticated error handling, you very likely don't need nested then handlers. Instead, use a flat chain and put the error handling logic at the end.

捕获后连锁

¥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:

js
doSomething()
  .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.

async/await 中,此代码如下所示:

¥In async/await, this code looks like:

js
async function main() {
  try {
    await doSomething();
    throw new Error("Something failed");
    console.log("Do this");
  } catch (e) {
    console.error("Do that");
  }
  console.log("Do this, no matter what happened before");
}

Promise 拒绝事件

¥Promise rejection events

如果承诺拒绝事件没有被任何处理程序处理,它就会冒泡到调用堆栈的顶部,并且主机需要将其显示出来。在 Web 上,每当 Promise 被拒绝时,两个事件之一就会发送到全局范围(通常,这是 window,或者,如果在 Web Worker 中使用,则为 Worker 或其他基于 Worker 的接口)。这两个事件是:

¥If a promise rejection event is not handled by any handler, it bubbles to the top of the call stack, and the host needs to surface it. On the web, 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:

unhandledrejection

当承诺被拒绝但没有可用的拒绝处理程序时发送。

rejectionhandled

当处理程序附加到已导致 unhandledrejection 事件的被拒绝的 Promise 时发送。

在这两种情况下,事件(类型 PromiseRejectionEvent)都具有 promise 属性(指示被拒绝的 Promise)作为成员,以及 reason 属性(提供拒绝 Promise 的原因)。

¥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.

这些使得为 Promise 提供后备错误处理成为可能,并帮助调试 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.

Node.js 中,处理 Promise 拒绝略有不同。你可以通过为 Node.js unhandledRejection 事件添加处理程序来捕获未处理的拒绝(请注意名称大小写的差异),如下所示:

¥In Node.js, handling promise rejection is slightly different. You capture unhandled rejections by adding a handler for the Node.js unhandledRejection event (notice the difference in capitalization of the name), like this:

js
process.on("unhandledRejection", (reason, promise) => {
  // Add code here to examine the "promise" and "reason" values
});

对于 Node.js,为了防止错误被记录到控制台(否则会发生的默认操作),只需添加 process.on() 监听器即可;不需要浏览器运行时的 preventDefault() 方法的等效方法。

¥For Node.js, to prevent the error from being logged to the console (the default action that would otherwise occur), adding that process.on() listener is all that's necessary; there's no need for an equivalent of the browser runtime's preventDefault() method.

但是,如果你添加了 process.on 监听器,但其中没有代码来处理被拒绝的 Promise,它们就会被扔在地板上并默默地被忽略。因此,理想情况下,你应该在该监听器中添加代码来检查每个被拒绝的 Promise,并确保它不是由实际的代码错误引起的。

¥However, if you add that process.on listener but don't also have code within it to handle rejected promises, they will just be dropped on the floor and silently ignored. So ideally, you should add code within that listener to examine each rejected promise and make sure it was not caused by an actual code bug.

作品

¥Composition

有四个 作文工具 用于同时运行异步操作:Promise.all()Promise.allSettled()Promise.any()Promise.race()

¥There are four composition tools for running asynchronous operations concurrently: Promise.all(), Promise.allSettled(), Promise.any(), and Promise.race().

我们可以同时开始操作并等待它们全部完成,如下所示:

¥We can start operations at the same time and wait for them all to finish like this:

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

如果数组中的 Promise 之一被拒绝,则 Promise.all() 立即拒绝返回的 Promise 并中止其他操作。这可能会导致意外的状态或行为。Promise.allSettled() 是另一个组合工具,可确保在解析之前完成所有操作。

¥If one of the promises in the array rejects, Promise.all() immediately rejects the returned promise and aborts the other operations. This may cause unexpected state or behavior. Promise.allSettled() is another composition tool that ensures all operations are complete before resolving.

这些方法都同时运行 Promise - 一系列 Promise 同时启动并且不互相等待。使用一些聪明的 JavaScript 可以实现顺序组合:

¥These methods all run promises concurrently — a sequence of promises are started simultaneously and do not wait for each other. Sequential composition is possible using some clever JavaScript:

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

在此示例中,我们将异步函数数组 reduce 到承诺链。上面的代码相当于:

¥In this example, we reduce an array of asynchronous functions down to a promise chain. The code above is equivalent to:

js
Promise.resolve()
  .then(func1)
  .then(func2)
  .then(func3)
  .then((result3) => {
    /* use result3 */
  });

这可以制作成可重用的组合函数,这在函数式编程中很常见:

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

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

composeAsync() 函数接受任意数量的函数作为参数,并返回一个新函数,该函数接受要通过组合管道传递的初始值:

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

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

顺序组合还可以使用 async/await 更简洁地完成:

¥Sequential composition can also be done more succinctly with async/await:

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

但是,在按顺序编写 Promise 之前,请考虑是否确实有必要 — 同时运行 Promise 总是更好,这样它们就不会不必要地相互阻塞,除非一个 Promise 的执行依赖于另一个 Promise 的结果。

¥However, before you compose promises sequentially, consider if it's really necessary — it's always better to run promises concurrently so that they don't unnecessarily block each other unless one promise's execution depends on another's result.

取消

¥Cancellation

Promise 本身没有用于取消的一流协议,但你可以直接取消底层异步操作,通常使用 AbortController

¥Promise itself has no first-class protocol for cancellation, but you may be able to directly cancel the underlying asynchronous operation, typically using AbortController.

围绕旧回调 API 创建 Promise

¥Creating a Promise around an old callback API

可以使用 构造函数 从头开始创建 Promise。这只需要封装旧的 API。

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

在理想的世界中,所有异步函数都已经返回 Promise。不幸的是,一些 API 仍然期望以旧方式传递成功和/或失败回调。最明显的例子是 setTimeout() 函数:

¥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:

js
setTimeout(() => saySomething("10 seconds passed"), 10 * 1000);

混合旧式回调和承诺是有问题的。如果 saySomething() 失败或包含编程错误,则不会捕获它。这是 setTimeout 设计的本质。

¥Mixing old-style callbacks and promises is problematic. If saySomething() fails or contains a programming error, nothing catches it. This is intrinsic to the design of setTimeout.

幸运的是,我们可以将 setTimeout 包裹在一个承诺中。最佳实践是将回调接受函数封装在尽可能低的级别,然后永远不再直接调用它们:

¥Luckily we can wrap setTimeout in a promise. The best practice is to wrap the callback-accepting functions at the lowest possible level, and then never call them directly again:

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

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

Promise 构造函数采用一个执行器函数,让我们可以手动解析或拒绝 Promise。由于 setTimeout() 并没有真正失败,因此我们在这种情况下省略了拒绝。有关执行器函数如何工作的更多信息,请参阅 Promise() 参考。

¥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. For more information on how the executor function works, see the Promise() reference.

定时

¥Timing

最后,我们将研究更多技术细节,有关何时调用注册的回调。

¥Lastly, we will look into the more technical details, about when the registered callbacks get called.

保证

¥Guarantees

在基于回调的 API 中,何时以及如何调用回调取决于 API 实现者。例如,回调可以同步或异步调用:

¥In the callback-based API, when and how the callback gets called depends on the API implementor. For example, the callback may be called synchronously or asynchronously:

js
function doSomething(callback) {
  if (Math.random() > 0.5) {
    callback();
  } else {
    setTimeout(() => callback(), 1000);
  }
}

强烈建议不要采用上述设计,因为它会导致所谓的 "扎尔戈州"。在设计异步 API 的上下文中,这意味着在某些情况下会同步调用回调,但在其他情况下会异步调用回调,从而给调用者带来歧义。有关更多背景信息,请参阅文章 设计异步 API,其中首次正式提出了该术语。这种 API 设计使得副作用难以分析:

¥The above design is strongly discouraged because it leads to the so-called "state of Zalgo". In the context of designing asynchronous APIs, this means a callback is called synchronously in some cases but asynchronously in other cases, creating ambiguity for the caller. For further background, see the article Designing APIs for Asynchrony, where the term was first formally presented. This API design makes side effects hard to analyze:

js
let value = 1;
doSomething(() => {
  value = 2;
});
console.log(value); // 1 or 2?

另一方面,Promise 是 控制反转 的一种形式 - API 实现者不控制回调何时被调用。相反,维护回调队列和决定何时调用回调的工作被委托给 Promise 实现,API 用户和 API 开发者都会自动获得强大的语义保证,包括:

¥On the other hand, promises are a form of inversion of control — the API implementor does not control when the callback gets called. Instead, the job of maintaining the callback queue and deciding when to call the callbacks is delegated to the promise implementation, and both the API user and API developer automatically gets strong semantic guarantees, including:

  • 使用 then() 添加的回调永远不会在 JavaScript 事件循环的 完成当前运行 之前调用。
  • 即使这些回调是在 Promise 表示的异步操作成功或失败之后添加的,也会被调用。
  • 通过多次调用 then() 可以添加多个回调。它们将按照插入的顺序依次被调用。

为了避免意外,传递给 then() 的函数永远不会被同步调用,即使有一个已经解决的 Promise:

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

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

传入的函数不是立即运行,而是被放入微任务队列中,这意味着它会稍后运行(仅在创建它的函数退出之后,并且当 JavaScript 执行堆栈为空时),就在控制权返回到事件之前 环形;即很快:

¥Instead of running immediately, the passed-in function is put on a microtask queue, which means it runs later (only after the function which created it exits, and when the JavaScript execution stack is empty), just before control is returned to the event loop; i.e. pretty soon:

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

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

任务队列与微任务

¥Task queues vs. microtasks

Promise 回调作为 microtask 处理,而 setTimeout() 回调作为任务队列处理。

¥Promise callbacks are handled as a microtask whereas setTimeout() callbacks are handled as task queues.

js
const promise = new Promise((resolve, reject) => {
  console.log("Promise callback");
  resolve();
}).then((result) => {
  console.log("Promise callback (.then)");
});

setTimeout(() => {
  console.log("event-loop cycle: Promise (fulfilled)", promise);
}, 0);

console.log("Promise (pending)", promise);

上面的代码将输出:

¥The code above will output:

Promise callback
Promise (pending) Promise {<pending>}
Promise callback (.then)
event-loop cycle: Promise (fulfilled) Promise {<fulfilled>}

欲了解更多详情,请参阅 任务与微任务

¥For more details, refer to Tasks vs. microtasks.

当承诺和任务发生冲突时

¥When promises and tasks collide

如果你遇到 Promise 和任务(例如事件或回调)以不可预测的顺序触发的情况,那么你可能会受益于使用微任务来检查状态或在有条件创建 Promise 时平衡你的 Promise。

¥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.

如果你认为微任务可能有助于解决此问题,请参阅 微任务指南 以了解有关如何使用 queueMicrotask() 将函数作为微任务排队的更多信息。

¥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.

也可以看看