借助AI翻译自Using Promise - JavaScript | MDN

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,再讲解如何创建它们。

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.

promise 本质上就是一个函数返回的对象,你把 callback(回调函数)挂到它上面,而不是把 callback 传进函数里。设想有一个函数 createAudioFileAsync(),它接收一个配置对象和两个 callback,异步生成一个音频文件:一个在音频文件创建成功时调用,另一个在出错时调用。

Here’s some code that uses createAudioFileAsync():

下面是使用 createAudioFileAsync() 的代码:

1
2
3
4
5
6
7
8
9
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);

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

如果把 createAudioFileAsync() 重写为返回一个 promise,你就可以把 callback 挂到它上面:

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

一个常见需求是依次执行两个或更多异步操作,后一个操作在前一个成功后启动,并接收前一步的结果。过去,连续执行多个异步操作会导致经典的 callback hell(回调地狱):

1
2
3
4
5
6
7
doSomething(function (result) {
doSomethingElse(result, function (newResult) {
doThirdThing(newResult, function (finalResult) {
console.log(\`Got the final result: ${finalResult}\`);
}, failureCallback);
}, failureCallback);
}, failureCallback);

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.

有了 promise,我们通过创建 promise chain(Promise 链)来实现这一点。Promise 的 API 设计在这方面非常出色,因为 callback 是挂在返回的 promise 对象上,而不是传进函数里。

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

奥妙在于:then() 函数返回一个新的 promise,与原来的不同:

1
2
const promise = doSomething();
const promise2 = promise.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.

这第二个 promise(promise2)代表的不只是 doSomething() 的完成,还包括你传入的 successCallbackfailureCallback 的完成 —— 而后两者本身也可能是返回 promise 的异步函数。在这种情况下,任何添加到 promise2 上的 callback 都会排在 successCallbackfailureCallback 返回的 promise 之后执行。

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

注意: 如果你想要一个可运行的示例来试验,可以用下面的模板创建任何返回 promise 的函数:

1
2
3
4
5
6
7
8
9
10
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);
});
}

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

具体实现会在下面的「Creating a Promise around an old callback API」一节讨论。

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:

利用这种模式,你可以创建更长的处理链,每个 promise 代表链中一个异步步骤的完成。此外,then 的参数是可选的,而 catch(failureCallback)then(null, failureCallback) 的简写 —— 所以如果所有步骤的错误处理代码都一样,你可以把它挂在链的末尾:

AI: Promise 链有个规则:某一步失败后,后面所有的成功回调都会被跳过,链会一路向下找第一个失败回调。在回调地狱里,每一步都必须自己写失败回调,因为回调没有 “自动往下传” 的机制:

1
2
3
4
5
6
7
8
9
10
11
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:

你也可能看到用 arrow function(箭头函数)来表达:

1
2
3
4
5
6
7
doSomething()
.then((result) => doSomethingElse(result))
.then((newResult) => doThirdThing(newResult))
.then((finalResult) => {
console.log(\`Got the final result: ${finalResult}\`);
})
.catch(failureCallback);

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

注意: Arrow function expression(箭头函数表达式)可以有隐式返回;因此 () => x() => { return x; } 的简写。

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

doSomethingElsedoThirdThing 可以返回任何值,如果返回的是一个 promise,链会等它执行完,再把成功后的结果交给下一个 callback,下一个 callback 拿到的是最终数据,不是 promise 本身。

AI解释 - 返回普通值(比如一个数字、一个对象):直接把这个值传给下一个 `then`
1
2
3
Promise.resolve(1)
.then(x => x + 1) // 返回数字 2
.then(y => console.log(y)) // 收到 2,打印 2
- 返回一个 promise:链不会把 promise 对象本身传下去,而是等这个 promise 跑完,再把它的结果值传给下一个 `then`。
1
2
3
fetch('/api/user')
.then(response => response.json()) // response.json() 返回一个 promise
.then(user => console.log(user)) // 收到的是 user 对象,不是 promise
---

这里有个原则一定要记住:then 的回调里只要启动了异步操作,就必须把它 return 出去,哪怕这个操作最后返回的是空值 undefined。如果上一个 handler 启动了一个 promise 但没有返回它,就再也无法追踪它的状态,这种 promise 被称为 floating promise。
(注:指在 then 回调中启动了异步操作但未将其返回,导致该 Promise 脱离链式追踪、无法获知其完成状态或错误的 Promise)。

1
2
3
4
5
6
7
8
9
10
11
12
13
doSomething()
.then((url) => {
// Missing \`return\` keyword in front of fetch(url).
// 在fetch(url)前缺少`return`关键字。
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.
// 结果为undefined,因为上一个处理函数没有返回任何内容
// 此时已经无法获知 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.

通过返回 fetch 调用的结果(它是一个 promise),我们既能追踪它的完成,也能在它完成时接收其值。

1
2
3
4
5
6
7
8
doSomething()
.then((url) => {
// \`return\` keyword added
return fetch(url);
})
.then((result) => {
// result is a Response object
});

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.

如果存在竞态条件(race condition),floating promise 的问题会更严重,如果上一个 handler 的 promise 没有返回,下一个 then handler 会被提前调用,它读取到的任何值都可能是不完整的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.
});

Therefore, as a rule of thumb, whenever your operation encounters a promise, return it and defer its handling to the next then handler.
因此,根据经验法则,每当你的操作遇到 Promise 对象时,应将其返回,并把对它的处理交给下一个 then handler。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.

更好的做法是,你可以把嵌套的 Promise 链拍平成一条链,这样更简单,错误处理也更容易。细节在下面的「Nesting」一节讨论。

1
2
3
4
5
6
7
8
9
doSomething()
.then((url) => fetch(url))
.then((res) => res.json())
.then((data) => {
listOfIngredients.push(data);
})
.then(() => {
console.log(listOfIngredients);
});

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

使用 async / await 可以让代码更直观,写起来也更像同步代码。下面是用 async / await 改写的同一个示例:

1
2
3
4
5
6
7
async function logIngredients() {
const url = await doSomething();
const res = await fetch(url);
const data = await res.json();
listOfIngredients.push(data);
console.log(listOfIngredients);
}

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

注意这段代码看起来和同步代码几乎一模一样,只是 promise 前面多了 await 关键字。不过它有个明显的缺点:容易漏掉 await。而且这个问题平时不会报错,只有等类型不匹配了(比如把 promise 当成普通值来用)才会暴露出来。

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 之上 —— 比如 doSomething() 还是原来那个函数,所以从 promise 迁移到 async / await 只需要极少的重构。你可以在 asyncawait 的参考文档中阅读更多关于 async / 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.

注意: async / await 与普通 promise 链具有相同的并发语义。一个 async function 中的 await 不会暂停整个程序,只会暂停依赖其返回值的那部分代码,所以在 await 等待期间,其他异步任务仍然可以运行。

Error handling

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

你可能还记得,前面的回调地狱写法中 failureCallback 出现了三次,而在 promise chain 末尾只出现了一次:

1
2
3
4
5
doSomething()
.then((result) => doSomethingElse(result))
.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:

如果发生异常,浏览器会沿着链向下查找 .catch() handler 或 onRejected。这很大程度上是模仿同步代码的工作方式:

1
2
3
4
5
6
7
8
try {
const result = syncDoSomething();
const newResult = syncDoSomethingElse(result);
const finalResult = syncDoThirdThing(newResult);
console.log(\`Got the final result: ${finalResult}\`);
} catch (error) {
failureCallback(error);
}

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

异步代码能写得跟同步代码一样,这种对应关系在 async / await 语法里体现得最彻底:

1
2
3
4
5
6
7
8
9
10
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);
}
}

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.

Promise 解决了回调地狱的一个根本缺陷:它能捕获所有错误,甚至包括抛出的异常和编程错误。这对于异步操作的函数式组合(functional composition)至关重要。现在所有错误都由链末尾的 catch() 方法处理,在不使用 async / await 的情况下,你几乎永远不需要使用 try / catch

Nesting

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.

在上面涉及 listOfIngredients 的示例中,第一个示例把一条 promise 链嵌套在另一个 then() handler 的返回值里,而第二个示例使用了完全扁平的链。简单的 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:

嵌套是一种用于限制 catch 语句作用域的控制结构。具体来说,嵌套的 catch 只捕获其作用域内及以下的失败,不捕获嵌套作用域之外、链中更上层的错误。正确使用时,这能让错误恢复更加精确:

1
2
3
4
5
6
7
8
9
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.

注意:此处的可选步骤是嵌套结构,这种嵌套并非由缩进产生,而是由包裹这些步骤的外层圆括号 () 的位置所决定。

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.

内部那个静默错误的 catch handler 只捕获来自 doSomethingOptional()doSomethingExtraNice() 的失败,之后代码继续执行 moreCriticalStuff()。重要的是,如果 doSomethingCritical() 失败,它的错误只会被最终(外层)的 catch 捕获,不会被内部的 catch handler 吞掉。

In async / await, this code looks like:

async / await 中,这段代码看起来是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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}\`);
}
}

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.

注意: 如果你没有复杂的错误处理需求,很可能根本不需要嵌套的 then handler。相反,使用扁平的链,把错误处理逻辑放在末尾即可。

Chaining after a 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 之后)继续往下链式调用是可以的,这对于在链中某个操作失败后仍要执行新操作非常有用。看下面的示例:

1
2
3
4
5
6
7
8
9
10
11
12
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:
这会输出以下文本:

1
2
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.
注意: 文本 “Do this” 未显示,原因是 “Something failed” 错误触发了拒绝拦截。

In async / await, this code looks like:
async / await 中,这段代码看起来是这样的:

1
2
3
4
5
6
7
8
9
10
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 rejection events

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:

如果一个 promise rejection 事件没有被任何 handler 处理,它会冒泡到调用栈的顶部,宿主环境需要把该异常暴露出来给开发者。。在 Web 环境中,每当一个 promise 被 reject,就会向全局作用域(global scope)发送两个事件中的一个。全局作用域通常是 window;如果在 Web Worker 中使用,则是 Worker 对象或其他基于 worker 的接口。这两个事件是:

unhandledrejection

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

在 promise 被 reject 但没有可用的 rejection handler 时发送。

rejectionhandled

Sent when a handler is attached to a rejected promise that has already caused an unhandledrejection event.

某个 Promise 已经报错抛过unhandledrejection警告,之后才给它加上错误处理函数时,就会触发这个事件。

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 属性指向被 reject 的 Promise 对象;reason 属性保存该 Promise 被 reject 的原因。

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 是每个上下文下全局生效的,因此无论错误来自哪里,所有错误都会交由同一套 event handler 处理。

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:

翻译:在 Node.js 中,处理 Promise 拒绝状态的方式略有区别。你可以通过给 Node.js 的 unhandledRejection 事件注册一个 handler,来捕获未被处理的 Promise 拒绝(注意这个事件名的大小写),示例如下:

1
2
3
4
process.on("unhandledRejection", (reason, promise) => {
// Add code here to examine the "promise" and "reason" values
// 在此处添加代码,以检查“promise”和“reason”的值
});

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.

在 Node.js 环境下,如果想要阻止错误输出打印到控制台(这是原本会发生的默认行为),只需要注册 process.on() 事件监听器就足够了;不需要使用浏览器运行环境里类似 preventDefault() 的方法。

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.

但是,如果你注册了这个 process.on 事件监听器,却没有在监听器内部编写逻辑去处理 Promise 拒绝异常,这些被拒绝的 Promise 就会被直接丢弃、静默忽略。因此理想做法是:在该监听器里增加代码,检查每一个被拒绝的 Promise,确认它并非真实代码 bug 导致。

Composition

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

有四个专门用来把多个异步任务放在一块儿并发执行的组合工具:Promise.all()Promise.allSettled()Promise.any()Promise.race()

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

我们可以像这样同时启动多个操作并等待它们全部完成:

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

If one of the promises in the array rejects, Promise.all() immediately rejects the returned promise. The other operations continue to run, but their outcomes are not available via the return value of Promise.all(). This may cause unexpected state or behavior. Promise.allSettled() is another composition tool that ensures all operations are complete before resolving.

如果数组里任意一个 Promise 失败(reject),Promise.all() 会立刻让返回的 Promise 失败。其余异步操作依旧会继续执行,但它们的结果无法从 Promise.all() 的返回值获取,这可能引发意外状态或异常行为。Promise.allSettled() 是另一个组合工具,它会等待全部操作执行完毕后才完成。

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:

这些方法都是并发执行 Promise,一组 Promise 会同时启动,互相之间不会等待。想要顺序执行(串行组合),则需要写一些巧妙的 JavaScript 代码才能实现。

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

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

在本示例中,我们将一组异步函数归约为一条 Promise 链。上述代码等价于:

1
2
3
4
5
6
7
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:

这可以被封装为一个可复用的组合函数(compose function),这在函数式编程中十分常见:

1
2
3
4
5
const applyAsync = (acc, val) => acc.then(val);
const composeAsync =
(...funcs) =>
(x) =>
funcs.reduce(applyAsync, Promise.resolve(x));

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:

composeAsync() 函数可以接收任意数量的函数作为参数,并返回一个新函数;这个新函数接收一个初始值,该初始值会在组合调用链路(管道)中依次传递执行。

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

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

顺序组合也可以借助 async/await 写得更加简洁。

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

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.

不过,在按顺序执行 Promise 之前,请先思考是否真的有必要这么做,并发运行 Promise 通常是更优选择,避免它们互相无谓阻塞;只有当一个 Promise 的执行依赖另一个的返回结果时,才适合顺序执行。

Cancellation

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

Promise 本身没有原生一等支持的取消协议,但你可以直接取消底层的异步操作,通常借助 AbortController 实现。

Creating a Promise around an old callback API

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。但现实是,部分接口依旧沿用旧式写法,需要传入成功/失败回调函数。最典型的例子就是 setTimeout()

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

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

混用旧式回调函数与 Promise 会产生问题。如果saySomething()执行失败或者存在程序错误,错误将无法被捕获。这是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:

幸运的是,我们可以将 setTimeout() 封装在 Promise 中。最佳实践是在尽可能底层的层级封装接收回调函数的方法,之后不再直接调用这些方法:

1
2
3
4
5
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

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

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.

Promise 构造器接受一个 executor function(执行器函数,注:即 new Promise(executor) 中传入的函数,在 Promise 创建时立即同步执行,接收 resolvereject 两个参数,用于手动控制 Promise 的完成或失败),它让我们可以手动 resolve 或 reject 一个 promise。由于 setTimeout() 实际上不会失败,我们在这个例子中省略了 reject。关于 executor function 如何工作的更多信息,请参阅 Promise() 参考文档。

Timing

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

最后,我们来看一些更底层的技术细节:注册的 callback 究竟在什么时候被调用。

Guarantees

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:

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

1
2
3
4
5
6
7
function doSomething(callback) {
if (Math.random() > 0.5) {
callback();
} else {
setTimeout(() => callback(), 1000);
}
}

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:

上面的设计是强烈不推荐的,因为它会导致所谓的 “state of Zalgo”(注:Zalgo 源自网络恐怖形象,技术圈用来指代 API 在某些情况下同步调用 callback、另一些情况下异步调用,导致调用方无法预测执行顺序的混乱状态)。在异步 API 设计的语境中,这意味着一个 callback 在某些情况下被同步调用,在另一些情况下被异步调用,给调用方造成了歧义。更多背景请参阅文章 Designing APIs for Asynchrony,该术语首次在其中被正式提出。这种 API 设计让 side effect(副作用)难以分析:

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

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:

另一方面,promise 是控制反转(inversion of control)的一种形式,API 实现者不控制 callback 何时被调用。相反,维护 callback 队列、决定何时调用 callback 的工作就交给了 promise 的实现来负责,API 的使用者和开发者都自动获得了可靠的语义保障,包括:

  • Callbacks added with then() will never be invoked before the completion of the current run of the JavaScript event loop.
  • These callbacks will be invoked even if they were added after the success or failure of the asynchronous operation that the promise represents.
  • Multiple callbacks may be added by calling then() several times. They will be invoked one after another, in the order in which they were inserted.

  • 通过 then() 添加的回调函数,绝不会在 JavaScript 当前这一轮事件循环执行完成之前被调用。

  • 即使这些回调函数是在 Promise 所代表的异步操作成功或失败之后添加的,它们仍会被调用。
  • 可以通过多次调用 then() 来添加多个回调函数。这些回调函数会按照添加的顺序依次执行。

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

为避免出现意外情况,传递给 then() 的函数永远不会被同步调用,即便传入的是一个已经 resolve 的 Promise:

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

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:

传入的函数不会立刻执行,而是被放进 microtask 队列。也就是说它会延后运行:必须等到创建该函数的函数执行完毕、JS 调用栈清空之后,在控制权交还给事件循环之前执行;换句话讲:会很快就跑。

1
2
3
4
5
6
7
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 callbacks are handled as a microtask whereas setTimeout() callbacks are handled as task queues.

Promise 的回调属于 microtask 任务,而 setTimeout 的回调属于宏任务(task queue)

1
2
3
4
5
6
7
8
9
10
11
12
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:

1
2
3
4
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

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.

如果你遇到某些场景,其中 Promise 与任务(例如事件或回调函数)以不可预测的顺序触发,当 Promise 是有条件创建时,你或许可以借助 microtask 来检查状态或是协调各个 Promise。

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.

如果你认为 microtask 或许有助于解决该问题,请查阅 microtask 指南,进一步了解如何使用 queueMicrotask() 将函数加入 microtask 队列。