Promise.resolve():创建一个以给定值完成的 PromisePromise.reject():创建一个以给定值拒绝的 Promise.then() 回调中返回和抛出.catch() 及其回调.finally() [ES2018]XMLHttpRequestutil.promisify()Promise.all()Promise.race()Promise.any() 和 AggregateError [ES2021]Promise.allSettled() [ES2020]Promise.all()(高级)Promise.all() 是 fork-joinPromise.all()Promise.race()Promise.any() [ES2021]Promise.allSettled() [ES2020] 推荐阅读
本章建立在前一章的基础上,介绍了 JavaScript 中的异步编程。
Promise 是一种用于异步传递结果的技术。
以下代码是使用基于 Promise 的函数 addAsync() 的示例(其实现将在稍后展示)
addAsync(3, 4)
.then(result => { // success
assert.equal(result, 7);
})
.catch(error => { // failure
assert.fail(error);
});Promise 类似于事件模式:有一个对象(一个 *Promise*),我们在其中注册回调
.then() 注册处理结果的回调。.catch() 注册处理错误的回调。基于 Promise 的函数返回一个 Promise,并在完成时(如果完成)向其发送结果或错误。Promise 将其传递给相关的回调。
与事件模式相比,Promise 针对一次性结果进行了优化
.then() 和 .catch(),因为它们都返回 Promise。这有助于顺序调用多个异步函数。稍后将详细介绍。什么是 Promise?有两种看待它的方式
这是一个基于 Promise 的函数的实现,它将两个数字 x 和 y 相加
function addAsync(x, y) {
return new Promise(
(resolve, reject) => { // (A)
if (x === undefined || y === undefined) {
reject(new Error('Must provide two parameters'));
} else {
resolve(x + y);
}
});
}addAsync() 立即调用 Promise 构造函数。该函数的实际实现位于传递给该构造函数的回调中(A 行)。该回调提供了两个函数
resolve 用于传递结果(成功的情况下)。reject 用于传递错误(失败的情况下)。图 22 描述了 Promise 可以处于的三种状态。Promise 专注于一次性结果,并保护我们免受 *竞争条件*(注册过早或过晚)的影响
.then() 回调或 .catch() 回调,则一旦 Promise 完成,就会通知它。.then() 或 .catch(),它们将收到缓存的值。此外,一旦 Promise 完成,其状态和完成值就不能再更改。这有助于使代码可预测并强制执行 Promise 的一次性性质。
有些 Promise 永远不会完成
Promise 可能永远不会完成。例如
new Promise(() => {})Promise.resolve():创建一个以给定值完成的 PromisePromise.resolve(x) 创建一个以值 x 完成的 Promise
Promise.resolve(123)
.then(x => {
assert.equal(x, 123);
});如果参数已经是一个 Promise,则按原样返回
const abcPromise = Promise.resolve('abc');
assert.equal(
Promise.resolve(abcPromise),
abcPromise);因此,给定一个任意值 x,我们可以使用 Promise.resolve(x) 来确保我们有一个 Promise。
请注意,名称是 resolve,而不是 fulfill,因为如果 .resolve() 的参数是一个被拒绝的 Promise,它将返回一个被拒绝的 Promise。
Promise.reject():创建一个以给定值拒绝的 PromisePromise.reject(err) 创建一个以值 err 拒绝的 Promise
const myError = new Error('My error!');
Promise.reject(myError)
.catch(err => {
assert.equal(err, myError);
});.then() 回调中返回和抛出.then() 处理 Promise 的完成。它还返回一个新的 Promise。该 Promise 如何完成取决于回调内部发生的情况。让我们来看看三种常见情况。
首先,回调可以返回一个非 Promise 值(A 行)。因此,.then() 返回的 Promise 将以该值完成(如 B 行中所检查)
Promise.resolve('abc')
.then(str => {
return str + str; // (A)
})
.then(str2 => {
assert.equal(str2, 'abcabc'); // (B)
});其次,回调可以返回一个 Promise p(A 行)。因此,p“成为”.then() 返回的内容。换句话说:.then() 已经返回的 Promise 被有效地替换为 p。
Promise.resolve('abc')
.then(str => {
return Promise.resolve(123); // (A)
})
.then(num => {
assert.equal(num, 123);
});为什么这很有用?我们可以返回基于 Promise 的操作的结果,并通过“扁平的”(非嵌套的).then() 处理其完成值。比较
// Flat
asyncFunc1()
.then(result1 => {
/*···*/
return asyncFunc2();
})
.then(result2 => {
/*···*/
});
// Nested
asyncFunc1()
.then(result1 => {
/*···*/
asyncFunc2()
.then(result2 => {
/*···*/
});
});第三,回调可以抛出异常。因此,.then() 返回的 Promise 将以该异常被拒绝。也就是说,同步错误被转换为异步错误。
const myError = new Error('My error!');
Promise.resolve('abc')
.then(str => {
throw myError;
})
.catch(err => {
assert.equal(err, myError);
});.catch() 及其回调.then() 和 .catch() 之间的区别在于,后者由拒绝触发,而不是由完成触发。但是,这两种方法都以相同的方式将回调的操作转换为 Promise。例如,在以下代码中,A 行中 .catch() 回调返回的值成为完成值
const err = new Error();
Promise.reject(err)
.catch(e => {
assert.equal(e, err);
// Something went wrong, use a default value
return 'default value'; // (A)
})
.then(str => {
assert.equal(str, 'default value');
});.then() 和 .catch() 始终返回 Promise。这使我们能够创建任意长的方法调用链
function myAsyncFunc() {
return asyncFunc1() // (A)
.then(result1 => {
// ···
return asyncFunc2(); // a Promise
})
.then(result2 => {
// ···
return result2 ?? '(Empty)'; // not a Promise
})
.then(result3 => {
// ···
return asyncFunc4(); // a Promise
});
}由于链接,A 行中的 return 返回最后一个 .then() 的结果。
在某种程度上,.then() 是同步分号的异步版本
.then() 顺序执行两个异步操作。我们还可以将 .catch() 添加到其中,并让它同时处理多个错误源
asyncFunc1()
.then(result1 => {
// ···
return asyncFunction2();
})
.then(result2 => {
// ···
})
.catch(error => {
// Failure: handle errors of asyncFunc1(), asyncFunc2()
// and any (sync) exceptions thrown in previous callbacks
});.finally() [ES2018]Promise 方法 .finally() 通常按如下方式使用
somePromise
.then((result) => {
// ···
})
.catch((error) => {
// ···
})
.finally(() => {
// ···
})
;.finally() 回调始终执行,而与 somePromise 以及 .then() 和/或 .catch() 返回的值无关。相反
somePromise 完成时,才会执行 .then() 回调。.catch() 回调somePromise 被拒绝,.then() 回调返回一个被拒绝的 Promise,.then() 回调抛出异常。.finally() 忽略其回调返回的内容,并简单地传递调用它之前存在的完成状态
Promise.resolve(123)
.finally(() => {})
.then((result) => {
assert.equal(result, 123);
});
Promise.reject('error')
.finally(() => {})
.catch((error) => {
assert.equal(error, 'error');
});但是,如果 .finally() 回调抛出异常,则 .finally() 返回的 Promise 将被拒绝
Promise.reject('error (originally)')
.finally(() => {
throw 'error (finally)';
})
.catch((error) => {
assert.equal(error, 'error (finally)');
});.finally() 的用例:清理.finally() 的一个常见用例类似于同步 finally 子句的常见用例:在使用完资源后进行清理。无论一切顺利还是出现错误,都应该始终这样做,例如
let connection;
db.open()
.then((conn) => {
connection = conn;
return connection.select({ name: 'Jane' });
})
.then((result) => {
// Process result
// Use `connection` to make more queries
})
// ···
.catch((error) => {
// handle errors
})
.finally(() => {
connection.close();
});.finally() 的用例:在任何类型的完成之后首先执行某些操作我们还可以在 .then() 和 .catch() 之前使用 .finally()。然后,我们在 .finally() 回调中执行的操作始终在其他两个回调之前执行。
例如,这就是完成的 Promise 会发生的情况
Promise.resolve('fulfilled')
.finally(() => {
console.log('finally');
})
.then((result) => {
console.log('then ' + result);
})
.catch((error) => {
console.log('catch ' + error);
})
;
// Output:
// 'finally'
// 'then fulfilled'这就是被拒绝的 Promise 会发生的情况
Promise.reject('rejected')
.finally(() => {
console.log('finally');
})
.then((result) => {
console.log('then ' + result);
})
.catch((error) => {
console.log('catch ' + error);
})
;
// Output:
// 'finally'
// 'catch rejected'在处理一次性结果时,Promise 相对于普通回调具有一些优势
基于 Promise 的函数和方法的类型签名更清晰:如果函数是基于回调的,则某些参数与输入有关,而末尾的一个或两个回调与输出有关。使用 Promise,所有与输出相关的内容都通过返回值处理。
链接异步处理步骤更方便。
Promise 可以处理异步错误(通过拒绝)和同步错误:在 new Promise()、.then() 和 .catch() 的回调内部,异常会被转换为拒绝。相反,如果我们对异步使用回调,则通常不会为我们处理异常;我们必须自己处理。
Promise 是一个单一标准,正在慢慢取代几种互不兼容的替代方案。例如,在 Node.js 中,许多函数现在都提供了基于 Promise 的版本。新的异步浏览器 API 通常都是基于 Promise 的。
Promise 的最大优势之一是不需要直接使用它们:它们是 *async 函数* 的基础,async 函数是一种用于执行异步计算的同步外观语法。异步函数将在下一章中介绍。
查看 Promise 的实际应用有助于理解它们。让我们看一些例子。
考虑以下文本文件 person.json,其中包含 JSON 数据
{
"first": "Jane",
"last": "Doe"
}让我们来看看两个版本的代码,它们读取这个文件并将其解析成一个对象。首先,基于回调的版本。其次,基于 Promise 的版本。
以下代码读取该文件的内容并将其转换为 JavaScript 对象。它基于 Node.js 风格的回调
import * as fs from 'fs';
fs.readFile('person.json',
(error, text) => {
if (error) { // (A)
// Failure
assert.fail(error);
} else {
// Success
try { // (B)
const obj = JSON.parse(text); // (C)
assert.deepEqual(obj, {
first: 'Jane',
last: 'Doe',
});
} catch (e) {
// Invalid JSON
assert.fail(e);
}
}
});fs 是一个内置的 Node.js 模块,用于文件系统操作。我们使用基于回调的函数 fs.readFile() 来读取名为 person.json 的文件。如果我们成功了,内容将通过参数 text 作为字符串传递。在 C 行,我们将该字符串从基于文本的数据格式 JSON 转换为 JavaScript 对象。JSON 是一个具有用于消费和生成 JSON 的方法的对象。它是 JavaScript 标准库的一部分,并在 本书后面 有详细介绍。
请注意,这里有两种错误处理机制:A 行中的 if 处理 fs.readFile() 报告的异步错误,而 B 行中的 try 处理 JSON.parse() 报告的同步错误。
以下代码使用 readFileAsync(),它是 fs.readFile() 的基于 Promise 的版本(通过 util.promisify() 创建,稍后解释)
readFileAsync('person.json')
.then(text => { // (A)
// Success
const obj = JSON.parse(text);
assert.deepEqual(obj, {
first: 'Jane',
last: 'Doe',
});
})
.catch(err => { // (B)
// Failure: file I/O error or JSON syntax error
assert.fail(err);
});函数 readFileAsync() 返回一个 Promise。在 A 行,我们通过该 Promise 的 .then() 方法指定了一个成功回调。then 回调中的其余代码是同步的。
.then() 返回一个 Promise,它允许在 B 行调用 Promise 方法 .catch()。我们使用它来指定一个失败回调。
请注意,.catch() 允许我们同时处理 readFileAsync() 的异步错误和 JSON.parse() 的同步错误,因为 .then() 回调内部的异常会变成拒绝。
XMLHttpRequest我们之前已经看到了用于在 Web 浏览器中下载数据的基于事件的 XMLHttpRequest API。以下函数对该 API 进行了 Promise 化
function httpGet(url) {
return new Promise(
(resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.responseText); // (A)
} else {
// Something went wrong (404, etc.)
reject(new Error(xhr.statusText)); // (B)
}
}
xhr.onerror = () => {
reject(new Error('Network error')); // (C)
};
xhr.open('GET', url);
xhr.send();
});
}请注意 XMLHttpRequest 的结果和错误是如何通过 resolve() 和 reject() 处理的
以下是使用 httpGet() 的方法
httpGet('http://example.com/textfile.txt')
.then(content => {
assert.equal(content, 'Content of textfile.txt\n');
})
.catch(error => {
assert.fail(error);
}); 练习:Promise 超时
exercises/promises/promise_timeout_test.mjs
util.promisify()util.promisify() 是一个实用函数,它将基于回调的函数 f 转换为基于 Promise 的函数。也就是说,我们正在从这种类型签名
f(arg_1, ···, arg_n, (err: Error, result: T) => void) : void
转换为这种类型签名
f(arg_1, ···, arg_n) : Promise<T>
以下代码对基于回调的 fs.readFile() 进行了 Promise 化(A 行)并使用它
import * as fs from 'fs';
import {promisify} from 'util';
const readFileAsync = promisify(fs.readFile); // (A)
readFileAsync('some-file.txt', {encoding: 'utf8'})
.then(text => {
assert.equal(text, 'The content of some-file.txt\n');
})
.catch(err => {
assert.fail(err);
}); 练习:
util.promisify()
util.promisify():exercises/promises/read_file_async_exrc.mjsutil.promisify():exercises/promises/my_promisify_test.mjs所有现代浏览器都支持 Fetch,这是一种新的基于 Promise 的 API,用于下载数据。可以将其视为 XMLHttpRequest 的基于 Promise 的版本。以下是 该 API 的摘录
interface Body {
text() : Promise<string>;
···
}
interface Response extends Body {
···
}
declare function fetch(str) : Promise<Response>;这意味着我们可以像这样使用 fetch()
fetch('http://example.com/textfile.txt')
.then(response => response.text())
.then(text => {
assert.equal(text, 'Content of textfile.txt\n');
}); 练习:使用 fetch API
exercises/promises/fetch_json_test.mjs
实现函数和方法的规则
不要混淆(异步)拒绝和(同步)异常。
这使得我们的同步和异步代码更具可预测性和更简单,因为我们始终可以专注于单一错误处理机制。
对于基于 Promise 的函数和方法,该规则意味着它们永远不应该抛出异常。唉,很容易不小心就弄错了——例如
// Don’t do this
function asyncFunc() {
doSomethingSync(); // (A)
return doSomethingAsync()
.then(result => {
// ···
});
}问题是,如果在 A 行抛出异常,那么 asyncFunc() 将抛出一个异常。该函数的调用者只期望拒绝,而没有为异常做好准备。我们可以通过三种方法来解决这个问题。
我们可以将函数的整个主体包装在一个 try-catch 语句中,并在抛出异常时返回一个被拒绝的 Promise
// Solution 1
function asyncFunc() {
try {
doSomethingSync();
return doSomethingAsync()
.then(result => {
// ···
});
} catch (err) {
return Promise.reject(err);
}
}鉴于 .then() 会将异常转换为拒绝,我们可以在 .then() 回调内部执行 doSomethingSync()。为此,我们通过 Promise.resolve() 启动一个 Promise 链。我们忽略了初始 Promise 的完成值 undefined。
// Solution 2
function asyncFunc() {
return Promise.resolve()
.then(() => {
doSomethingSync();
return doSomethingAsync();
})
.then(result => {
// ···
});
}最后,new Promise() 也会将异常转换为拒绝。因此,使用此构造函数类似于前面的解决方案
// Solution 3
function asyncFunc() {
return new Promise((resolve, reject) => {
doSomethingSync();
resolve(doSomethingAsync());
})
.then(result => {
// ···
});
}大多数基于 Promise 的函数都按如下方式执行
以下代码演示了这一点
function asyncFunc() {
console.log('asyncFunc');
return new Promise(
(resolve, _reject) => {
console.log('new Promise()');
resolve();
});
}
console.log('START');
asyncFunc()
.then(() => {
console.log('.then()'); // (A)
});
console.log('END');
// Output:
// 'START'
// 'asyncFunc'
// 'new Promise()'
// 'END'
// '.then()'我们可以看到,new Promise() 的回调在代码结束之前执行,而结果在稍后传递(A 行)。
这种方法的好处
同步开始有助于避免竞争条件,因为我们可以依赖于基于 Promise 的函数开始的顺序。在 下一章 中有一个例子,其中文本被写入文件并避免了竞争条件。
链接 Promise 不会使其他任务的处理时间不足,因为在 Promise 结束之前,总会有一个中断,在此期间事件循环可以运行。
基于 Promise 的函数总是异步返回结果;我们可以确定永远不会有同步返回。这种可预测性使代码更容易处理。
有关此方法的更多信息
“为异步设计 API”,作者:Isaac Z. Schlueter
组合器模式 是函数式编程中用于构建结构的一种模式。它基于两种函数
当涉及到 JavaScript Promise 时
基本函数包括:Promise.resolve()、Promise.reject()
组合器包括:Promise.all()、Promise.race()、Promise.any()、Promise.allSettled()。在每种情况下
接下来,我们将仔细研究一下提到的 Promise 组合器。
Promise.all()这是 Promise.all() 的类型签名
Promise.all<T>(promises: Iterable<Promise<T>>): Promise<Array<T>>Promise.all() 返回一个 Promise,它
promises 都已完成,则完成。promises 的完成值的数组。这是一个输出 Promise 被完成的快速演示
const promises = [
Promise.resolve('result a'),
Promise.resolve('result b'),
Promise.resolve('result c'),
];
Promise.all(promises)
.then((arr) => assert.deepEqual(
arr, ['result a', 'result b', 'result c']
));以下示例演示了如果至少有一个输入 Promise 被拒绝会发生什么
const promises = [
Promise.resolve('result a'),
Promise.resolve('result b'),
Promise.reject('ERROR'),
];
Promise.all(promises)
.catch((err) => assert.equal(
err, 'ERROR'
));图 23 说明了 Promise.all() 的工作原理。
Promise.all() 实现异步 .map()数组转换方法(如 .map()、.filter() 等)适用于同步计算。例如
function timesTwoSync(x) {
return 2 * x;
}
const arr = [1, 2, 3];
const result = arr.map(timesTwoSync);
assert.deepEqual(result, [2, 4, 6]);如果 .map() 的回调是一个基于 Promise 的函数(将普通值映射到 Promise 的函数),会发生什么?那么 .map() 的结果是一个 Promise 数组。唉,这不是普通代码可以处理的数据。值得庆幸的是,我们可以通过 Promise.all() 来解决这个问题:它将 Promise 数组转换为一个 Promise,该 Promise 将使用普通值数组完成。
function timesTwoAsync(x) {
return new Promise(resolve => resolve(x * 2));
}
const arr = [1, 2, 3];
const promiseArr = arr.map(timesTwoAsync);
Promise.all(promiseArr)
.then(result => {
assert.deepEqual(result, [2, 4, 6]);
});.map() 示例接下来,我们将使用 .map() 和 Promise.all() 从 Web 下载文本文件。为此,我们需要以下工具函数
function downloadText(url) {
return fetch(url)
.then((response) => { // (A)
if (!response.ok) { // (B)
throw new Error(response.statusText);
}
return response.text(); // (C)
});
}downloadText() 使用基于 Promise 的 fetch API 将文本文件下载为字符串
response(A 行)。response.ok(B 行)检查是否有错误,例如“找不到文件”。.text()(C 行)将文件的内容检索为字符串。在以下示例中,我们下载了两个文本文件
const urls = [
'http://example.com/first.txt',
'http://example.com/second.txt',
];
const promises = urls.map(
url => downloadText(url));
Promise.all(promises)
.then(
(arr) => assert.deepEqual(
arr, ['First!', 'Second!']
));Promise.all() 的简单实现这是 Promise.all() 的简化实现(例如,它不执行安全检查)
function all(iterable) {
return new Promise((resolve, reject) => {
let elementCount = 0;
let result;
let index = 0;
for (const promise of iterable) {
// Preserve the current value of `index`
const currentIndex = index;
promise.then(
(value) => {
result[currentIndex] = value;
elementCount++;
if (elementCount === result.length) {
resolve(result); // (A)
}
},
(err) => {
reject(err); // (B)
});
index++;
}
if (index === 0) {
resolve([]);
return;
}
// Now we know how many Promises there are in `iterable`.
// We can wait until now with initializing `result` because
// the callbacks of .then() are executed asynchronously.
result = new Array(index);
});
}结果 Promise 被解决的两个主要位置是 A 行和 B 行。其中一个解决后,另一个就不能再更改解决值了,因为一个 Promise 只能被解决一次。
Promise.race()这是 Promise.race() 的类型签名
Promise.race<T>(promises: Iterable<Promise<T>>): Promise<T>Promise.race() 返回一个 Promise q,一旦 promises 中的第一个 Promise p 被解决,它就会被解决。q 具有与 p 相同的解决值。
在以下演示中,已完成的 Promise 的解决(A 行)发生在被拒绝的 Promise 的解决(B 行)之前。因此,结果也被完成了(C 行)。
const promises = [
new Promise((resolve, reject) =>
setTimeout(() => resolve('result'), 100)), // (A)
new Promise((resolve, reject) =>
setTimeout(() => reject('ERROR'), 200)), // (B)
];
Promise.race(promises)
.then((result) => assert.equal( // (C)
result, 'result'));在下一个演示中,拒绝先发生
const promises = [
new Promise((resolve, reject) =>
setTimeout(() => resolve('result'), 200)),
new Promise((resolve, reject) =>
setTimeout(() => reject('ERROR'), 100)),
];
Promise.race(promises)
.then(
(result) => assert.fail(),
(err) => assert.equal(
err, 'ERROR'));请注意,Promise.race() 返回的 Promise 会在其输入 Promise 中的第一个 Promise 被解决后立即解决。这意味着 Promise.race([]) 的结果永远不会被解决。
图 24 说明了 Promise.race() 的工作原理。
Promise.race() 使 Promise 超时在本节中,我们将使用 Promise.race() 使 Promise 超时。以下辅助函数将在多次使用时很有用
function resolveAfter(ms, value=undefined) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(value), ms);
});
}resolveAfter() 返回一个 Promise,该 Promise 在 ms 毫秒后使用 value 解决。
此函数使 Promise 超时
function timeout(timeoutInMs, promise) {
return Promise.race([
promise,
resolveAfter(timeoutInMs,
Promise.reject(new Error('Operation timed out'))),
]);
}timeout() 返回一个 Promise,其解决与以下两个 Promise 中先解决的那个相同
promisetimeoutInMs 毫秒后被拒绝的 Promise为了生成第二个 Promise,timeout() 利用了这样一个事实,即使用被拒绝的 Promise 解决一个待定的 Promise 会导致前者被拒绝。
让我们看看 timeout() 的实际效果。在这里,输入 Promise 在超时之前就已完成。因此,输出 Promise 也已完成。
timeout(200, resolveAfter(100, 'Result!'))
.then(result => assert.equal(result, 'Result!'));在这里,超时发生在输入 Promise 完成之前。因此,输出 Promise 被拒绝。
timeout(100, resolveAfter(2000, 'Result!'))
.catch(err => assert.deepEqual(err, new Error('Operation timed out')));重要的是要理解“使 Promise 超时”的真正含义
也就是说,超时只会阻止输入 Promise 影响输出(因为一个 Promise 只能被解决一次)。但它不会停止生成输入 Promise 的异步操作。
Promise.race() 的简单实现这是 Promise.race() 的简化实现(例如,它不执行安全检查)
function race(iterable) {
return new Promise((resolve, reject) => {
for (const promise of iterable) {
promise.then(
(value) => {
resolve(value); // (A)
},
(err) => {
reject(err); // (B)
});
}
});
}结果 Promise 在 A 行或 B 行被解决。一旦被解决,解决值就不能再更改了。
Promise.any() 和 AggregateError [ES2021]这是 Promise.any() 的类型签名
Promise.any<T>(promises: Iterable<Promise<T>>): Promise<T>Promise.any() 返回一个 Promise p。它的解决方式取决于参数 promises(它指的是一个 Promise 的可迭代对象)
p 将使用该 Promise 解决。p 将使用 AggregateError 的实例拒绝,该实例包含所有拒绝值。这是 AggregateError(Error 的子类)的类型签名
class AggregateError extends Error {
// Instance properties (complementing the ones of Error)
errors: Array<any>;
constructor(
errors: Iterable<any>,
message: string = '',
options?: ErrorOptions // ES2022
);
}
interface ErrorOptions {
cause?: any; // ES2022
}图 25 展示了 Promise.any() 的工作原理。
如果一个 Promise 成功解决,就会发生这种情况
const promises = [
Promise.reject('ERROR A'),
Promise.reject('ERROR B'),
Promise.resolve('result'),
];
Promise.any(promises)
.then((result) => assert.equal(
result, 'result'
));如果所有 Promise 都被拒绝,就会发生这种情况
const promises = [
Promise.reject('ERROR A'),
Promise.reject('ERROR B'),
Promise.reject('ERROR C'),
];
Promise.any(promises)
.catch((aggregateError) => assert.deepEqual(
aggregateError.errors,
['ERROR A', 'ERROR B', 'ERROR C']
));Promise.any() 与 Promise.all() 的比较Promise.any() 和 Promise.all() 可以通过两种方式进行比较
Promise.all():第一个输入拒绝会拒绝结果 Promise,或者其解决值为包含输入解决值的数组。Promise.any():第一个输入解决会解决结果 Promise,或者其拒绝值为包含输入拒绝值的数组(在错误对象内部)。Promise.all() 关注的是*所有*解决。相反的情况(至少有一个拒绝)会导致拒绝。Promise.any() 关注的是第一个解决。相反的情况(只有拒绝)会导致拒绝。Promise.any() 与 Promise.race() 的比较Promise.any() 和 Promise.race() 也有关联,但关注的是不同的事情
Promise.race() 关注的是结果。首先得到结果的 Promise “获胜”。换句话说:我们想知道哪个异步计算首先终止。Promise.any() 关注的是解决。首先得到解决的 Promise “获胜”。换句话说:我们想知道哪个异步计算首先成功。.race() 的主要(相对罕见)用例是 Promise 超时。.any() 的用例更广泛。我们接下来会看看。
Promise.any() 的用例如果我们有多个异步计算,并且我们只对第一个成功的计算感兴趣,则使用 Promise.any()。在某种程度上,我们让计算相互竞争,并使用最快的那个。
以下代码演示了下载资源时的样子
const resource = await Promise.any([
fetch('http://example.com/first.txt')
.then(response => response.text()),
fetch('http://example.com/second.txt')
.then(response => response.text()),
]);相同的模式使我们能够使用下载速度更快的任何模块
const lodash = await Promise.any([
import('https://primary.example.com/lodash'),
import('https://secondary.example.com/lodash'),
]);为了进行比较,如果辅助服务器只是一个备用服务器(以防主服务器出现故障),我们会使用以下代码
let lodash;
try {
lodash = await import('https://primary.example.com/lodash');
} catch {
lodash = await import('https://secondary.example.com/lodash');
}Promise.any()?Promise.any() 的简单实现基本上是 Promise.all() 实现的镜像版本。
Promise.allSettled() [ES2020]这一次,类型签名稍微复杂一些。您可以随意跳到第一个演示,它应该更容易理解。
这是 Promise.allSettled() 的类型签名
Promise.allSettled<T>(promises: Iterable<Promise<T>>)
: Promise<Array<SettlementObject<T>>>它返回一个数组的 Promise,该数组的元素具有以下类型签名
type SettlementObject<T> = FulfillmentObject<T> | RejectionObject;
interface FulfillmentObject<T> {
status: 'fulfilled';
value: T;
}
interface RejectionObject {
status: 'rejected';
reason: unknown;
}Promise.allSettled() 返回一个 Promise out。一旦所有 promises 都已得到结果,out 就会使用一个数组得到解决。该数组的每个元素 e 对应于 promises 的一个 Promise p
如果 p 使用解决值 v 得到解决,则 e 为
{ status: 'fulfilled', value: v }如果 p 使用拒绝值 r 被拒绝,则 e 为
{ status: 'rejected', reason: r }除非在迭代 promises 时出错,否则输出 Promise out 永远不会被拒绝。
图 26 展示了 Promise.allSettled() 的工作原理。
Promise.allSettled() 的第一个演示这是 Promise.allSettled() 工作原理的快速入门演示
Promise.allSettled([
Promise.resolve('a'),
Promise.reject('b'),
])
.then(arr => assert.deepEqual(arr, [
{ status: 'fulfilled', value: 'a' },
{ status: 'rejected', reason: 'b' },
]));Promise.allSettled() 的更长示例下一个示例类似于.map() 加 Promise.all() 示例(我们从中借用了函数 downloadText()):我们正在下载多个文本文件,这些文件的 URL 存储在一个数组中。但是,这一次,我们不想在出现错误时停止,我们想继续下去。Promise.allSettled() 允许我们这样做
const urls = [
'http://example.com/exists.txt',
'http://example.com/missing.txt',
];
const result = Promise.allSettled(
urls.map(u => downloadText(u)));
result.then(
arr => assert.deepEqual(
arr,
[
{
status: 'fulfilled',
value: 'Hello!',
},
{
status: 'rejected',
reason: new Error('Not Found'),
},
]
));Promise.allSettled() 的简单实现这是 Promise.allSettled() 的简化实现(例如,它不执行安全检查)
function allSettled(iterable) {
return new Promise((resolve, reject) => {
let elementCount = 0;
let result;
function addElementToResult(i, elem) {
result[i] = elem;
elementCount++;
if (elementCount === result.length) {
resolve(result);
}
}
let index = 0;
for (const promise of iterable) {
// Capture the current value of `index`
const currentIndex = index;
promise.then(
(value) => addElementToResult(
currentIndex, {
status: 'fulfilled',
value
}),
(reason) => addElementToResult(
currentIndex, {
status: 'rejected',
reason
}));
index++;
}
if (index === 0) {
resolve([]);
return;
}
// Now we know how many Promises there are in `iterable`.
// We can wait until now with initializing `result` because
// the callbacks of .then() are executed asynchronously.
result = new Array(index);
});
}对于 Promise 组合器,*短路*意味着输出 Promise 会提前得到结果 - 在所有输入 Promise 都得到结果之前。以下组合器会短路
Promise.all():一旦一个输入 Promise 被拒绝,输出 Promise 就会被拒绝。Promise.race():一旦一个输入 Promise 得到结果,输出 Promise 就会得到结果。Promise.any():一旦一个输入 Promise 得到解决,输出 Promise 就会得到解决。再次强调,提前得到结果并不意味着被忽略的 Promise 背后的操作会停止。它只是意味着它们的解决被忽略了。
Promise.all()(高级)考虑以下代码
const asyncFunc1 = () => Promise.resolve('one');
const asyncFunc2 = () => Promise.resolve('two');
asyncFunc1()
.then(result1 => {
assert.equal(result1, 'one');
return asyncFunc2();
})
.then(result2 => {
assert.equal(result2, 'two');
});以这种方式使用 .then() 会*顺序*执行基于 Promise 的函数:只有在 asyncFunc1() 的结果得到结果后,才会执行 asyncFunc2()。
Promise.all() 帮助更并发地执行基于 Promise 的函数
Promise.all([asyncFunc1(), asyncFunc2()])
.then(arr => {
assert.deepEqual(arr, ['one', 'two']);
});确定异步代码“并发”程度的提示:关注异步操作何时开始,而不是它们的 Promise 如何处理。
例如,以下每个函数都并发执行 asyncFunc1() 和 asyncFunc2(),因为它们几乎同时启动。
function concurrentAll() {
return Promise.all([asyncFunc1(), asyncFunc2()]);
}
function concurrentThen() {
const p1 = asyncFunc1();
const p2 = asyncFunc2();
return p1.then(r1 => p2.then(r2 => [r1, r2]));
}另一方面,以下两个函数都顺序执行 asyncFunc1() 和 asyncFunc2():asyncFunc2() 仅在 asyncFunc1() 的 Promise 得到解决后才被调用。
function sequentialThen() {
return asyncFunc1()
.then(r1 => asyncFunc2()
.then(r2 => [r1, r2]));
}
function sequentialAll() {
const p1 = asyncFunc1();
const p2 = p1.then(() => asyncFunc2());
return Promise.all([p1, p2]);
}Promise.all() 是 fork-joinPromise.all() 与并发模式“fork join”松散相关。让我们回顾一下我们之前遇到的一个例子
Promise.all([
// (A) fork
downloadText('http://example.com/first.txt'),
downloadText('http://example.com/second.txt'),
])
// (B) join
.then(
(arr) => assert.deepEqual(
arr, ['First!', 'Second!']
));本节提供链接 Promise 的技巧。
问题
// Don’t do this
function foo() {
const promise = asyncFunc();
promise.then(result => {
// ···
});
return promise;
}计算从 asyncFunc() 返回的 Promise 开始。但之后,计算继续,并通过 .then() 创建另一个 Promise。foo() 返回前一个 Promise,但应该返回后一个 Promise。解决方法如下
function foo() {
const promise = asyncFunc();
return promise.then(result => {
// ···
});
}问题
// Don’t do this
asyncFunc1()
.then(result1 => {
return asyncFunc2()
.then(result2 => { // (A)
// ···
});
});A 行中的 .then() 是嵌套的。扁平结构会更好
asyncFunc1()
.then(result1 => {
return asyncFunc2();
})
.then(result2 => {
// ···
});这是另一个可以避免的嵌套示例
// Don’t do this
asyncFunc1()
.then(result1 => {
if (result1 < 0) {
return asyncFuncA()
.then(resultA => 'Result: ' + resultA);
} else {
return asyncFuncB()
.then(resultB => 'Result: ' + resultB);
}
});我们可以再次获得扁平结构
asyncFunc1()
.then(result1 => {
return result1 < 0 ? asyncFuncA() : asyncFuncB();
})
.then(resultAB => {
return 'Result: ' + resultAB;
});在以下代码中,我们实际上受益于嵌套
db.open()
.then(connection => { // (A)
return connection.select({ name: 'Jane' })
.then(result => { // (B)
// Process result
// Use `connection` to make more queries
})
// ···
.finally(() => {
connection.close(); // (C)
});
})我们在 A 行中接收异步结果。在 B 行中,我们进行嵌套,以便我们可以在回调内部和 C 行中访问变量 connection。
问题
// Don’t do this
class Model {
insertInto(db) {
return new Promise((resolve, reject) => { // (A)
db.insert(this.fields)
.then(resultCode => {
this.notifyObservers({event: 'created', model: this});
resolve(resultCode);
}).catch(err => {
reject(err);
})
});
}
// ···
}在 A 行中,我们正在创建一个 Promise 来传递 db.insert() 的结果。这过于冗长,可以简化
class Model {
insertInto(db) {
return db.insert(this.fields)
.then(resultCode => {
this.notifyObservers({event: 'created', model: this});
return resultCode;
});
}
// ···
}关键是我们不需要创建一个 Promise;我们可以返回 .then() 调用的结果。另一个好处是我们不需要捕获并重新拒绝 db.insert() 的失败。我们只需将其拒绝传递给 .insertInto() 的调用者。
除非另有说明,否则该功能是在 ECMAScript 6 中引入的(这是 Promise 被添加到该语言中的时间)。
词汇表
Promise.all()Promise.all<T>(promises: Iterable<Promise<T>>)
: Promise<Array<T>>P 的**解决**:如果所有输入 Promise 都得到解决。P 的**拒绝**:如果一个输入 Promise 被拒绝。Promise.race()Promise.race<T>(promises: Iterable<Promise<T>>)
: Promise<T>P 的**结果**:如果第一个输入 Promise 得到结果。Promise.any() [ES2021]Promise.any<T>(promises: Iterable<Promise<T>>): Promise<T>P 的**解决**:如果一个输入 Promise 得到解决。P 的**拒绝**:如果所有输入 Promise 都被拒绝。AggregateError。这是 AggregateError 的类型签名(省略了一些成员)
class AggregateError {
constructor(errors: Iterable<any>, message: string);
get errors(): Array<any>;
get message(): string;
}Promise.allSettled() [ES2020]Promise.allSettled<T>(promises: Iterable<Promise<T>>)
: Promise<Array<SettlementObject<T>>>P 的**解决**:如果所有输入 Promise 都得到结果。P 的**拒绝**:如果在迭代输入 Promise 时出错。这是 SettlementObject 的类型签名
type SettlementObject<T> = FulfillmentObject<T> | RejectionObject;
interface FulfillmentObject<T> {
status: 'fulfilled';
value: T;
}
interface RejectionObject {
status: 'rejected';
reason: unknown;
}