译: JavaScript中的并发Promise
文章翻译至: Effective Promise Concurrency in JavaScript
目录
async function getPageData() {
const user = await fetchUser()
const product = await fetchProduct()
}
这段代码是否可以继续优化?使其在原来一半的时间内完成?
在此函数中,我们依次等待user
的获取和product
的获取。
但其中一个并不依赖于另一个,因此我们不必等待其中一个完成后才发出下一个请求。
相反,我们可以同时触发这两个请求,并同时等待这两个请求。
使用 Promise.all
Promise.all() 静态方法接受一个 Promise 可迭代对象作为输入,并返回一个 Promise。当所有输入的 Promise 都被fulfills时,返回的 Promise 也将被fulfills(即使传入的是一个空的可迭代对象),并返回一个包含所有fulfills值的数组。如果输入的任何 Promise 被rejects,则返回的 Promise 将被rejects,并带有第一个被rejects的原因。
一种方法是利用 Promise.all ,如下所示:
async function getPageData() {
const [user, product] = await Promise.all([
fetchUser(), fetchProduct()
])
}
现在,如果我们想象,每个请求都需要 1 秒来完成,而在我们原来的函数中,我们将连续等待两个请求总共 2 秒才能完成我们的函数。在这个新函数中,我们**同时开启了两个请求*,因此我们的函数在 1 秒内完成 —— 所需时间相比之前缩短了一半!
但是……还有一个问题
问题
首先,我们在这里根本没有处理Error。
这时你可能说: “sure,我会把它放在一个大的 try-catch
块中”。
async function getPageData() {
try {
const [user, product] = await Promise.all([
fetchUser(), fetchProduct()
])
} catch (err) {
// 🚩 this has a big problem...
}
}
但这实际上有一个重大问题。
假设 fetchUser
函数 首先完成但出现了错误并被抛出。这将会触发try catch
,然后该函数继续执行。
这里有一个关键问题: 如果 fetchProduct
函数也有一个错误,这个错误就不会再触发 catch
了。因为函数已经继续执行到catch
内,之后getPageData
将执行完成。
完整Demo:
async function getPageData() {
try {
const [user, product] = await Promise.all([
fetchUser(), fetchProduct()
])
console.log(user, product); // This code will not be executed. because catch...
} catch (err) {
console.log('err is', err); // err is fetchUser Error
}
}
const fetchUser = (e) => {
return new Promise((reject) => {
throw new Error('fetchUser Error')
})
};
const fetchProduct = (e) => {
return new Promise((reject) => {
throw new Error('fetchProduct Error')
});
};
这将导致一个promise reject
未被处理。
如果我们有某种逻辑,它提示用户或者保存错误日志,如下所示:
// ...
} catch (err) {
handle(err)
}
// ...
function handle(err) {
alertToUser(err)
saveToLoggingService(err)
}
我们只会获取到第一个错误。第二个错误将丢失,没有用户反馈,也不会在我们的错误日志中出现。
使用 .catch() 解决
解决上述问题的一种方法是将函数传递给 .catch()
,例如如下所示:
function onReject(err) {
handle(err)
return err
}
async function getPageData() {
const [user, product] = await Promise.all([
fetchUser().catch(onReject), // ⬅️
fetchProduct().catch(onReject) // ⬅️
])
if (user instanceof Error) {
handle(user) // ✅
}
if (product instanceof Error) {
handle(product) // ✅
}
}
在这种情况下,如果我们收到error,我们将返回error handle并处理error。如果我们得到的 user
和 product
是 Error对象 ,我们可以用 instanceof
检查属于哪个函数。
完整测试Demo:
async function getPageData() {
const [user, product] = await Promise.all([
fetchUser().catch(onReject),
fetchProduct().catch(onReject)
])
if (user instanceof Error) {
console.log('user error:', user);
}
if (product instanceof Error) {
console.log('product error:', product); // 打印: product error: Error: fetchProduct Error
}
console.log(user); // ✅
}
const fetchUser = (e) => {
return new Promise((resolve) => {
resolve('✅')
})
};
const fetchProduct = (e) => {
return new Promise(() => {
throw new Error('fetchProduct Error')
});
};
const onReject = (err) => {
// 上报错误
toReportError(err);
return err
};
const toReportError = (e) => { };
getPageData();
但是,这里的主要缺点是我们需要在整个代码中始终提供 .catch(onReject)
。这在实际开发中很容易忘记。
实例化后单独 await
附带说明一下,记住我们并不总是需要在创建 Promise
后立即await它,我们可以延后Promise的await。
async function getPageData() {
// Fire both requests together
const userPromise = fetchUser().catch(onReject)
const productPromise = fetchProduct().catch(onReject)
// Await together
const user = await userPromise
const product = await productPromise
// Handle individually
if (user instanceof Error) {
handle(user)
}
if (product instanceof Error) {
handle(product)
}
}
完整Demo:
async function getPageData() {
const userPromise = fetchUser().catch(onReject)
const productPromise = fetchProduct().catch(onReject)
const user = await userPromise
const product = await productPromise
if (user instanceof Error) {
console.log('user error:', user);
}
if (product instanceof Error) {
console.log('product error:', product); // 打印: product error: Error: fetchProduct Error
}
console.log(user); // 打印: ✅
}
const fetchUser = (e) => {
return new Promise((resolve) => {
resolve('✅')
})
};
const fetchProduct = (e) => {
return new Promise(() => {
throw new Error('fetchProduct Error')
});
};
const onReject = (err) => {
toReportError(err);
return err
};
const toReportError = (e) => { };
getPageData();
此外,在这种方式中,如果我们想,我们可以更安全地使用 try/catch
而不会出现之前遇到的问题:
async function getPageData() {
const userPromise = fetchUser().catch(onReject)
const productPromise = fetchProduct().catch(onReject)
// Try/catch each
try {
const user = await userPromise
} catch (err) {
handle(err)
}
try {
const product = await productPromise
} catch (err) {
handle(err)
}
}
在这三个版本中,我个人喜欢 Promise.all
版本,因为“一起await这两个函数”感觉更常用。但话虽如此,我认为这取决于个人喜好。
Promise.allSettled
Promise.allSettled() 静态方法将一个 Promise 可迭代对象作为输e。当所有输入的 Promise 都已fulfills或reject时(包入,并返回一个单独的 Promis括传入空的可迭代对象时),返回的 Promise 将被fulfills,并带有描述每个 Promise 结果的对象数组。
使用 ·Promise.allSettled`,我们不是直接返回 user 和 product,而是得到一个包含每个 Promise 结果的值或错误的结果对象。
async function getPageData() {
const [userResult, productResult] = await Promise.allSettled([
fetchUser(), fetchProduct()
])
}
结果对象有 3 个属性:
- status - 两个值,"fulfilled" 或 "rejected"。
- value - 仅当 status 为 "fulfilled" 时才出现。promise的值。
- reason - 仅当 status 为 "rejected" 时才出现。promise被拒绝的原因。
因此,我们现在可以读取每个promise的状态,并单独处理每个错误,而不会丢失任何关键信息:
async function getPageData() {
// Fire and await together
const [userResult, productResult] = await Promise.allSettled([
fetchUser(), fetchProduct()
])
// Process user
if (userResult.status === 'rejected') {
const err = userResult.reason
handle(err)
} else {
const user = userResult.value
}
// Process product
if (productResult.status === 'rejected') {
const err = productResult.reason
handle(err)
} else {
const product = productResult.value
}
}
在抽象一下:
async function getPageData() {
const results = await Promise.allSettled([
fetchUser(), fetchProduct()
])
// Nicer on the eyes
const [user, product] = handleResults(results)
}
我们可以实现一个简单的 handleResults
函数,如下所示:
// Generic function to throw if any errors occured, or return the responses
// if no errors happened
function handleResults(results) {
const errors = results
.filter(result => result.status === 'rejected')
.map(result => result.reason)
if (errors.length) {
// Aggregate all errors into one
throw new AggregateError(errors)
}
return results.map(result => result.value)
}
我们可以在这里使用一个巧妙的技巧,即 AggergateError
类,来抛出一个可能包含多个内部的错误。这样,当捕获时,我们会通过 AggregateError
上的 .errors
属性获得包含所有详细信息的单个错误,其中包含所有错误:
async function getPageData() {
const results = await Promise.allSettled([
fetchUser(), fetchProduct()
])
try {
const [user, product] = handleResults(results)
} catch (err) {
for (const error of err.errors) {
handle(error)
}
}
}
Promise.race
Promise.race() 静态方法接受一个 promise 可迭代对象作为输入,并返回一个 Promise。这个返回的 promise 状态随第一个 promise 的状态改变而改变。
例如,我们可以实现一个简单的超时,如下所示:
// Race to see which Promise completes first
const racePromise = Promise.race([
doSomethignSlow(),
new Promise((resolve, reject) =>
// Time out after 5 seconds
setTimeout(() => reject(new Error('Timeout')), 5000)
)
])
try {
const result = await racePromise
} catch (err) {
// Timed out!
}
注意:这并不总是理想的做法,因为一般来说,如果出现超时,应尽可能取消未完成的待办任务。
例如,如果 doSomethingSlow()
获取了数据,我们通常希望使用 AbortController
在超时时中止获取数据,而不是仅仅reject promise。
但这只是一个演示基本概念的示例
另外,像往常一样,最好也处理所有承诺的所有错误:
const racePromise = Promise.race([
doSomethignSlow().catch(onReject), // ✅
// ...
])
Promise.any
Promise.any() 静态方法将一个 Promise 可迭代对象作为输入,并返回一个 Promise。当输入的任何一个 Promise fulfills时,这个返回的 Promise 将会fulfills,并返回第一个fulfills的值。如果所有输入的 Promise 都被reject(包括传递了空的可迭代对象)时,它会以一个包含reject原因的数组。
例如,在无法预测某个数据哪个获取更快的情况下,这可能很有用:
const anyPromise = Promise.any([
getSomethingFromPlaceA(), getSomethingFromPlaceB()
])
try {
const winner = await anyPromise
} catch (err) {
// Darn, both failed
}
与上面类似,这里的理想解决方案是一旦较快的请求完成,就中止较慢的请求。但同样,这些只是为了演示基础知识而设计的简单示例。
处理未经处理的reject:
const anyPromise = Promise.any([
getSomethingFromPlaceA().catch(onReject), // ✅
getSomethingFromPlaceB().catch(onReject) // ✅
])
三点注意事项...
- Don’t get carried way
并发很好,但过度的并行会导致网络、磁盘性能或其他问题。避免这样的疯狂:
// ❌ please don't
const results = await Promise.allSettled([
fetchUser(),
fetchProduct(),
getAnotherThing(),
andAnotherThing(),
andYetAnotherThing(),
andMoreThings(),
areYouEvenStillReading(),
thisIsExcessive(),
justPleaseDont()
])
- 我们是在同时等待,而不是同时执行
为了避免混淆,我想指出的是,重要的是要意识到,当我们在这里谈论并发时,我们指的并发是等待promise,而不是并发执行代码。
JavaScript 一直以来都是一种单线程语言,所以我们不要忘记这一点。
- 不要过早优化
有时,顺序代码更容易推理和管理。
// It sure is simple and easy to read, isn't it
async function getPageData() {
const user = await fetchUser()
const product = await fetchProduct()
}