全面理解 Promise 系列(三):多个 Promise 协同工作
小举 筑基

我们有时候希望通过监听多个 Promise 的进展来确定下一步的行动。 JavaScript 提供了几种方法来监听多个 Promise,并以略微不同的方式对它们做出响应。

3.1 Promise.all()

Promise.all() 方法可以接受一个 iterable (例如数组)作为参数,该参数包含所需要监听的 Promise, 并返回一个 Promise。当且仅当该 iterable 对象中的每个 Promise 都被履行(fulfilled)时,它才会返回一个处于履行状态(fulfilled)的 Promise。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let promise1 = Promise.resolve(42);

let promise2 = new Promise((resolve, reject) => {
resolve(43);
});

let promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(44);
}, 100);
});

let promise4 = Promise.all([promise1, promise2, promise3]);

promise4.then((result) => {
console.log(Array.isArray(result)); // true
console.log(result[0]); // 42
console.log(result[1]); // 43
console.log(result[2]); // 44
});

这里的每个被履行的 promise 都有相对应的一个值, 对 promise.all() 的调用创建了 promise4。 当 promise1, promise2 和 promise3 都被履行时, promise4 才会被履行。传递给 promise4 的履行处理器的是一个包含 42,43,44 的数组, 这些值是按照传递给 promise.all()的 promise 的顺序存储的。这样一来, 我们就可以将 promise 的结果与履行它们的 promise 相匹配。

如果传递给 promise.all() 的某个 promise 被拒绝, 那么返回的 promise 会被立刻拒绝,而不会等待其他的 promise 执行完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let promise1 = Promise.resolve(42);

let promise2 = new Promise((resolve, reject) => {
reject(43);
});

let promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(44);
}, 100);
});

let promise4 = Promise.all([promise1, promise2, promise3]);

promise4.then((reason) => {
console.log(Array.isArray(reason)); // false
console.log(reason); // 43
});

在这个例子中,第二个 promise 被拒绝,其值为 43。 promise4 的拒绝处理器被立即调用, 而不是等待第一个 Promise 或第三个 Promise 执行完成。 (它们仍然会执行完成,只不过 Promise4 不会等待执行结果。) _拒绝处理器总是接收单一的值,而不是接收数组_。 这个值是被拒绝的 Promise 的值。在本例中,我们传递给拒绝处理器的值为 43,以表明拒绝来自 promise2。

⚠️ iterable 参数中的任何非 Promise 值都会被传递给 Promise.resolve() 并被转换为 Promise。

3.1.1 何时使用 Promise.all() 方法

Promise.all() 适用于任何需要等待多个 Promise 履行的情况,这其中任何一个 Promise 被拒绝都会导致整个操作失败,下面介绍 Promise.all() 的一些常见用例

  1. 同时处理多个文件

当使用如 Node.js 或 Deno 这样的服务器端 JavaScript 运行环境时,我们可能需要读取多个文件以处理其中的数据。在这种情况下,最高效的做法是并行读取文件,并等待它们全部被读取后再继续处理数据。下面是一个在 Node.js 中读取文件的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { readFile } from 'node:fs/promises';

function readFiles(filenames) {
return Promise.all(filenames.map((filename) => readFile(filename, 'utf8')));
}

readFiles(['file1.json', 'file2.json', 'file3.json'])
.then((fileContents) => {
// 解析 JSON 数据
const data = fileContents.map((fileContent) => JSON.parse(fileContent));

// 进行必要的处理
cnosole.log(data);
})
.catch((error) => {
console.error(error);
});

这个例子使用 Node.js 基于 Promise 的文件系统 API 来并行读取多个文件。readFiles()函数接受一个由待读取文件名组成的数组作为参数, 然后将每个文件名映射到 入readFile() 函数创建的 Promise 对象。文件以文本形式读取。(这一点从第二个参数 utf8 可知)。其读取结果在履行处理器中作为fileContents 数组可用,其中包含每个文件中的文本。在这里,文件内容被解析为 JSON 格式并存储在 data 数组中, 然后再被传递给处理数据的函数。这是处理多个文件的常用方法, 因为如果任何一个文件不能被读取或解析, 那么整个操作就不能正确完成,应该及时停止。

  1. 调用多个相互依赖的 Web 服务 API

Promise.all() 的另一个常见用例是调用多个相互依赖的 Web 服务 API, 这在 REST API 中尤为常见,因为与实体相关的每一种数据类型都可能有自己的端点。考虑这样一个场景:每个用户都拥有博客文章和相册,我们需要在用户的个人资料中展示这些信息。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const API_BASE = 'https://jsonplaceholder.typicode.com';

function fetchUserData(userId) {
const urls = [
`${API_BASE}/users/${userId}/posts`,
`${API_BASE}/users/${userId}/albums`,
];
return Promise.all(urls.map((url) => fetch(url)));
}

function createError(response) {
return new Error(
`Unexpected status code: ${response.status} ${response.statusText} for ${response.url}`
);
}

fetchUserData(1)
.then((responses) => {
return Promise.all(
responses.map((response) => {
if (response.ok) {
return response.json();
} else {
return Promise.reject(createError(response));
}
})
);
})
.then((data) => {
const [posts, albums] = data;

// 对数据进行必要的处理
console.log(posts);
console.log(albums);
})
.catch((error) => console.error(error));

在以上代码中,我们为每个用户提供端点 /posts/albumsfetchUserData()函数接受一个用户 ID 并生成要调用的 URL。接着 URL 被映射到每个fetch()调用所返回的 Promise。当收到响应后,另一个Promise.all() 调用将每个响应映射到另一个 Promise。这个 Promise 要么是 JSON 正文(如果响应状态码介于 200 和 299 之间),要么是处于拒绝状态的 Promise,这将中断整个操作并调用拒绝处理器。在最后的履行处理器中, posts 和 albums 的数据准备就绪,等待处理。

  1. 人为引入延迟

有时我们想延迟一些事情的发生,与服务器端相比,这种情况更可能发生在浏览器端。比如我们有时需要在用户操作和响应之间引入延迟。具体的说,我们可能想在从服务器获取数据期间显示一个加载指示器。但如果响应太快,那么用户可能看不到加载动画,因而无法判断屏幕上的数据是否为最新数据。在这种情况下,我们可以人为引入延迟如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const API_BASE = 'https://jsonplaceholder.typicode.com';
const appElement = document.getElementById('app');

function delay(mileseconds) {
return new Promise((resolve) => {
setTimeout(() => resolve(), mileseconds);
});
}

function fetchUserData(userId) {
appElement.classList.add('loading');
const urls = [
`${API_BASE}/users/${userId}/posts`,
`${API_BASE}/users/${userId}/albums`,
];

return Promise.all([...urls.map((url) => fetch(url)), delay(2000)]).then(
(results) => {
// 去除 delay() 中的未定义结果
return results.slice(0, -1);
}
);
}

function createError(response) {
return new Error(
`Unexpected status code: ${response.status} ${response.statusText} for ${response.url}`
);
}

fetchUserData(1)
.then((responses) => {
return Promise.all(
responses.map((response) => {
if (response.ok) {
return response.json();
} else {
return Promise.reject(createError(response));
}
})
);
})
.then((data) => {
const [posts, albums] = data;

// 对数据进行必要的处理
console.log(posts);
console.log(albums);
})
.finally(() => {
appElement.classList.remove('loading');
})
.catch((error) => console.error(error));

这段代码在前一个例子的基础上,为每一个 fetch() 调用引入了延迟。delay() 函数返回一个 promise。该 promise 会在指定的毫秒数过去后履行,实现方法是使用 setTimeout() 函数,并传递一个调用 resolve() 的回调函数。请注意,这种情况下,无需向 resolve() 传递任何值,因为没有相关数据。

⚠️ 我们也可以直接将 resolve 作为第一个参数传递给 setTimeout()。但是某些 javascript 运行环境会向超时回调函数传递参数。为了在各种运行环境中实现最佳的兼容性,最好从另一个函数内部调用 resolve()

fetchUserData() 函数发起对指定用户 ID 的 Web 服务请求。 和之前的例子一样,promise.all() 被用来同时监控多个 fetch 请求。但在这个例子中,传递给 promise.all() 的 promise 数组还包含对 delay() 的调用。当返回的 promise 被履行时,履行处理器会收到包含所有结果的数组。其中最后一个元素是 undefined。在最终结果从 fetchUserData() 返回之前移除最后一个元素,这样调用 fetchUserData()的代码就根本不需要知道 delay() 被调用了。CSS 类 loading 被添加给 DOM 中的应用元素,以表明数据正在被检索, 随后在数据就绪时,被解决处理器删除。

以上便是 promise.all() 的一些常见用例。如果一个 promise 被拒绝了,但是我们还想继续操作下去,又该怎么办呢?在这种情况下,promise.allSettled() 方法是更好的选择。

3.2 Promise.allSettled() 方法

Promise.allSettled() 方法在 Promise.all() 方法的基础上做了细微的改变。该方法会等待 iterable 对象中所有的 Promise 都被解决,不管它们是被履行还是被拒绝Promise.allSettled() 返回的值总是一个处于履行状态的 Promise,它带有一个结果对象数组。

对于处于履行状态的 Promise 而言,其结果对象有两个属性:

  • status:该属性总是被设置为 fulfilled (履行)。
  • value:这是该 Promise 被履行的值。

对于处于拒绝状态的 Promise 而言,其结果对象也有两个属性:

  • status:该属性总是被设置为 rejected (拒绝)。
  • reason:这是该 Promise 被拒绝的值。

我们可以使用返回的对象数组来确定每个 Promise 的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let promise1 = Promise.resolve(42);
let promise2 = Promise.reject(43);
let promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(44);
}, 100);
});

let promise4 = Promise.allSettled([promise1, promise2, promise3]);

promise4.then((results) => {
console.log(results[0].status); // "fulfilled"
console.log(results[0].value); // 42

console.log(results[1].status); // "rejected"
console.log(results[1].reason); // 43

console.log(results[2].status); // "fulfilled"
console.log(results[2].value); // 44
});

尽管第二个 Promise 处于拒绝状态,但是对于 Promise.allSettled() 的调用仍然会返回一个处于履行状态的 Promise。

3.1.1 何时使用 Promise.allSettled() 方法

Promise.allSettled() 方法的许多用例和 Promise.all() 相同。不过它最适合用于忽略操作被拒绝、以不同方式处理拒绝或允许部分成功的情况。下面是一些常见的用例:

  1. 分别处理多个文件

在讨论 Promise.all() 时,我们看到了处理多个文件的例子。这些文件相互依赖,只有当所有文件都处理成功后,整个操作才能成功。在另一些情况下,我们分别处理多个文件。如果一个文件处理失败,那么我们无需停止整个操作,而是继续处理其他文件,同时记录处理失败的操作,以便之后重试.下面是一个在 node.js 中处理多个文件的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { readFile, writeFile } from 'node:fs/promises';

// 或者在文件上进行其他操作
function transformText(text) {
return text.split('').reverse().join('');
}

function transformFiles(filenames) {
return Promise.allSettled(
filenames.map((filename) => {
readFile(filename, 'utf8')
.then((text) => transformText(text))
.then((transformedText) => writeFile(filename, transformedText))
.catch((reason) => {
reason.filename = filename;
return Promise.reject(reason);
});
})
);
}

transformFiles(['file1.txt', 'file2.txt', 'file3.txt']).then((results) => {
// 得到失败的任务
const failedTasks = results.filter((result) => result.status === 'rejected');
if (failedTasks.length > 0) {
console.error(`Failed to transform ${failedTasks.length} files:`);
failedTasks.forEach((task) => {
console.error(`- ${task.reason.filename}: ${task.reason.message}`);
});
} else {
console.log('All files transformed successfully.');
}
});

这段代码读取一系列文件,颠倒文件中的文本顺序,然后将这些文本写回原始文件。当然,你可以用其他任何操作来代替 transformText(), transformFiles() 函数接收一个由文件名组成的数组,读取文件的内容,转换文本,并将转换后的文本写回文件。链式 promise 体现了这个过程中的每一步。拒绝处理器,为拒绝理由添加了属性 filename, 以便事后更容易解释结果。当对所有文件的操作都完成后,我们对 results 进行过滤,找出转换失败的任务,然后将失败结果输出到控制台。在生产环境中,最好把失败结果送入一个监控系统或一个队列中,以便再次尝试转换。

  1. 调用多个相互独立的 Web 服务 API

并且我们希望所有的请求都能成功。如果无需所有的请求都成功,那么我们可以使用 Promise.allSettled()。 回顾之前的例子,如果可以显示用户资料页面,即使有些数据丢失也没关系,那么我们就可以通过使用 Promise.allSettled() 来避免向用户显示错误消息。示例代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const API_BASE = 'https://jsonplaceholder.typicode.com';

function fetchUserData(userId) {
const urls = [
`${API_BASE}/users/${userId}/posts`,
`${API_BASE}/users/${userId}/albums`,
`${API_BASE}/users/${userId}/extras`,
];
return Promise.allSettled(urls.map((url) => fetch(url))).then((results) => {
return results.map((result) => result.value);
});
}

fetchUserData(1)
.then((responses) => {
return Promise.all(
response.map((resposne) => {
if (response?.ok) {
return response.json();
}
})
);
})
.then((results) => {
const [posts, albums, extras] = results;

if (posts) {
console.log('Posts:', posts);
}

if (albums) {
console.log('Albums:', albums);
}

if (extras) {
console.log('Extras:', extras);
}
})
.catch((reason) => {
console.error('Failed to fetch user data:', reason);
});

这个例子还调用了第三个端点,即:${API_BASE}/users/${userId}/extras, 因为这个端点并不存在,所以代码将返回 404。一旦所有请求都完成,履行处理器就会将每个结果映射到对应的 value 属性,这样做确保任何处于拒绝状态的 promise 都会被映射到 undefined,并且处于履行状态的 promise 被映射到从 fetch() 返回的响应对象。

首先,因为 response 可能是 undefined,所以我们需要在检查 OK 属性之前确定 response 是真值,然后读取每个有效响应的 JSON 主体,最后由旅行处理器读取数据。因为不能保证每个请求的数据都存在,所以我们需要在处理数据之前检查每个值是否存在。

  1. 等候动画播放完成

在一个网页中,元素可以同时以多种方式实现动画化。比如我们可以使一个元素从网页的底部移动到顶部,同时改变该元素的宽度和高度,使其逐渐进入视野。在这种情况下,最好在对元素或网页进行下一步修改之前,等候所有的动画播放完成。代码如下:

1
2
3
4
5
6
7
8
9
10
11
function waitForAnimations(element) {
return Promise.allSettled(
element.getAnimations().map((animation) => animation.finished)
);
}

const toastElementer = document.getElementById('toaster');

waitFormAnimations(toastElementer).then(() => {
console.log('Toaster animations complete.');
});

在这个例子中,我们既不关心是否有任何动画播放失败,也不关心是否在动画播放过程中收到任何值。因此,Promise.allSettled()Promise.all() 更合适。getAnimations() 方法返回一个动画对象数组,其中每个对象都有一个包含 promise 的属性 finished。当动画播放完成时,这个 promise 就会进入解决状态,通过将每个 promise 传入 Promise.allSettled(), 我们将在所有动画都播放完成时得到通知。因为 Promise.allSettled() 永远不会返回处于拒绝状态的 promise,所以我们可以直接附加一个履行处理器,而不用担心漏掉任何未捕获的拒绝错误。

3.3 Promise.any() 方法

Promise.any() 方法接收一个包含多个 promise 的 iterable 对象,并在传入的任何 promise 被履行时返回一个处于履行状态的 promise。一旦其中一个 promise 被履行,该操作就会提前完成,(这一点与Promise.all()相反,在Promise.all() 中,一旦有一个 promise 被拒绝,操作就会提前完成。)下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
let promise1 = Promise.reject(43);
let promise2 = Promise.resolve(42);
let promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(44);
}, 100);
});

let promise4 = Promise.any([promise1, promise2, promise3]);

promise4.then((result) => {
console.log(result); // 42
});

在这个例子中,尽管第一个 promise 被拒绝了,但对 Promise.any() 调用还是成功了。因为第二个 promise 被成功履行,第三个 promise 的结果则被忽略。

如果传递给 Promise.any() 的所有 promise 都被拒绝,那么 Promise.any() 将返回一个处于拒绝状态的 promise, 并且拒绝的理由是 AggregateError, AggregateError 是一个 错误,它代表了存储在属性 errors 中的多个错误。举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let promise1 = Promise.reject(43);
let promise2 = new Promise((resolve, reject) => {
reject(44);
});
let promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject(45);
});
});

let promise4 = Promise.any([promise1, promise2, promise3]);

promise4.catch((reason) => {
console.log(reason.message); // true

console.log(reason.errors[0]); // 43
console.log(reason.errors[1]); // 44
console.log(reason.errors[2]); // 45
});

在这里 Promise.any() 收到的 promise 都没有被履行,因此返回的 promise 以 AggregateError 为由被拒绝。可以检查属性 errors,它是一个数组,可用来检索每个 promise 的拒绝值。

3.3.1 何时使用 Promise.any() 方法

Promise.any() 方法最适合这样的情况。我们希望传入的任何一个 promise 被成功履行即可,而不关心有多少其他 promise 被拒绝,除非他们都被拒绝。下面介绍一些常见用例:

  1. 执行对冲请求

对冲请求是指客户端向多台服务器发出请求,并接受第一个回复的响应。这在客户端需要最小化延迟,且有服务器资源,专门用于管理额外负载和重复响应的情况下,很有用。来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const HOSTS = ['api1.example-url', 'api2.example-url'];

function hedgedFetch(endpoint) {
return Promise.any(
HOSTS.map((hostname) => fetch(`https://${hostname}${endpoint}`))
);
}

hedgedFetch('/transactions')
.then((response) => {
console.log(response);
})
.catch((error) => {
console.error(error.message);
});

每个对冲请求都会调用一个主机数组 hedgedFetch()函数。根据这些主机名,创建一个 fetch() 请求数组,并将该数组传递给 Promise.any(), 即使实际上有多个请求,但在用户看来也只有一个请求。这样一来,用户只用使用一个履行处理器和一个拒绝处理器即可处理操作结果。就算有一个请求失败了,用户也不会知道,只有当所有请求都失败时,javascript 才会调用拒绝处理器。

  1. 在 service worker 中使用最快速的响应

使用 service worker 的网页通常可以选择是从网络还是从缓存中加载数据。在某些情况下,网络请求可能比从缓存中加载更快。因此我们可能想使用 Promise.any() 来选择更快的响应。下面这段代码说明了 service worker 是如何工作的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
self.addEventListener('fetch', (event) => {
// 获取缓存响应
const cachedResponse = caches.match(event.request);

// 获取新的响应
const fetchedResponse = fetch(event.request);

// 以最佳方案响应
event
.respondWith(
Promise.any([cachedResponse, fetchedResponse.catch(() => cachedResponse)])
)
.then((response) => {
response ?? fetchedResponse; // 选择最佳响应
})
.catch(() => {});
});

使用 fetch 事件监听器,我们可以监听网络请求并拦截响应。 这个关于 service work 的例子使用 fetch 事件监听器从缓存(使用caches.match())和网络中(使用 fetch())读取数据,caches.match()的调用总是返回一个处于履行状态的 promise。其结果要么是匹配的响应对象,要么是 undefined(如果请求不再缓存中)。event.respondWith() 方法,需要传递一个 promise, 因此,这个事件处理器传递了 Promise.any() 的结果。

Promise.any() 接收两个 promise, 一个是获取的响应,它带有拒绝处理器的,并且该拒绝处理器默认返回来自缓存的响应,另一个是缓存的响应本身。通过这种方式,如果缓存中存在满足要求的数据或者获取网络请求被拒绝,则 javascript 都将返回来自缓存的响应。然后履行处理器需要确保收到的响应是有效的。(请注意,如果缓存首先响应但未命中,则 response 可能为 undefined)。拒绝处理器不执行任何操作,因为在这种情况下,没有备选方案,由于获取的响应和缓存的响应都被拒绝,因此 javascript 会悄悄的忽略错误,以允许浏览器采取默认行为。

虽然 Promise.any() 在第一个成功履行的 promise 出现后就会提前完成,但我们也可以根据第一个已解决(可以是履行也可以是拒绝)的 promise来提前完成整个操作。而不管结果如何,对于这种情况,我们可以使用 Promise.race()

3.4 Promise.race() 方法

在监控多个 promise 时,Promise.race()方法与 Promise.any() 方法略有不同,这个方法也接受一个包含多个 promise 的 iterable 对象并返回一个 promise。但返回的 promise 在第一个 promise 确定状态 就立即确定其状态。Promise.race()不像 Promise.all() 那样等待所有 promise 都被履行,也不像 Promise.any() 那样在第一个 promise 被履行时即提前完成,而是一旦数组中的任何 promised 状态确定(不管成功还是失败都叫做状态已确定),就会返回一个 promise。来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let promise1 = Promise.resolve(42);
let promise2 = new Promise((resolve, reject) => {
resolve(43);
});
let promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(44);
}, 100);
});

let promise4 = Promise.race([promise1, promise2, promise3]);

promise4.then((result) => {
console.log(result); // 42
});

在这段代码中,第一个 promise 处于履行状态,其他 promise 则对应之后要执行的任务。随后 promise4 的履行处理器的调用,其值为 42,而其他 promise 被忽略,传递递给 Promise.race() 的 promise, 互相竞争,看哪个先确定状态,如果首先确定状态的 promise 是履行状态,那么返回的 promise 也将处于履行状态,如果首先确定状态的 promise 是拒绝状态,那么返回的 promise 也将处于拒绝状态。以拒绝状态为例,我们来看一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(44);
}, 100);
});
let promise2 = new Promise((resolve, reject) => {
reject(43);
});
let promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(44);
}, 50);
});

let promise4 = Promise.race([promise1, promise2, promise3]);

promise4.catch((reason) => {
console.log(reason); // 43
});

在这段代码中,promise1 和 promise3 都使用 setTimeout() 来延迟履行时间,结果是 promise4 被拒绝。因为 promise2 在 promise1 或 promise3 被解决之前就被拒绝了。尽管 promise1 和 promise3 最终都成功履行,但他们仍被忽略。因履行状态在 promise2 被拒绝之后才确定。

3.4.1 何时使用 Promise.race() 方法

如果我们希望能够提前完成涉及多个 promise 的操作,那么 Promise.race() 方法是最佳选择。在使用 Promise.any() 时,我们希望其中一个 promise 成功,并且仅在所有 promise 都失败时才给予关注。与此不同,在使用Promise.race() 时,即使一个 promise 失败,只要他在任何其他 promise 成功之前失败,我们也希望得知这个情况。下面介绍 Promise.race() 的常见用例:

  1. 为操作设置超时

虽然 fetch() 函数有很多有用的功能,但它无法为一个给定的请求管理超时(timeout),一个请求会处于挂起状态,直到该请求以某种方式完成。我们可以通过使用 Promise.race() 来轻松的创建一个封装方法,为任何请求添加超时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function timeout(millseconds) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Timeout'));
}, millseconds);
});
}

function fetchWithTimeout(url, timeoutMs) {
return Promise.race([fetch(url), timeout(timeoutMs)]);
}

const API_URL = 'https://jsonplaceholder.typicode.com/users';

fetchWithTimeout(API_URL, 5000)
.then((response) => {
console.log(response);
return response.json();
})
.then((users) => {
console.log(users);
})
.catch((error) => {
console.error(error.message);
});

timeout() 函数和我们之前创建的 delay() 函数类似, 只不过它在延迟后调用 reject(), (delay 函数则是调用 resolve())。在这种情况下,延迟表示发生了错误。因为我们想在某请求所花的时间超预期时被告知。 fetchWithTimeout() 函数随后在一个数组中调用 fetch()timeout() 并将其传递给了 Promise.race()。如果对 fetch() 的调用所花的时间超过了 timeout() 所规定的时间,返回的 promise 就会被拒绝,以便我们进行适当的处理。

‼️ 请记住,即使 fetchWithTimeout() 拒绝了一个超过指定时间的请求,该请求也不会被取消,它将继续在后台等待响应,即使该响应应该被忽略。

3.5 总结

如果想同时监控和响应多个 promise, 那么我们可以采用 javascript 提供的多种方法,每种方法略有不同,但都让我们能够并行的运行多个 promise,并将它们作为一个整体来响应。

  • Promise.all(): 当且仅当所有 promise 都被履行时,返回的 promise 才会处于履行状态。若任何一个 promise 被拒绝,则返回的 promise 就处于拒绝状态的。

  • Promise.allSettled(): 返回的 Promise 总是处于履行状态,并且带有一个结果对象数组。

  • Promise.any(): 一旦有一个 promise 被履行,返回的 promise 就处于履行状态。而当所有的 promise 都被拒绝时,返回的 promise 就处于拒绝状态。

  • Promise.race(): 返回的 promise 总是处于第一个被确定状态的 promise 的状态。如果首先确定状态的 promise 是进入履行状态,那么返回的 promise 也将处于旅行状态。如果首先确定状态的 promise 是进入拒绝状态,那么返回的 promise 也将处于拒绝状态。