Promise.all
用于优化多个同时处理的异步请求,降低时间
例如
async function getPageData() {const user = await fetchUser()const product = await fetchProduct()
}
在此函数中,我们依次等待获取用户数据和产品数据。
但是这两个相互之间不存在依赖依赖,所以我们不必等待一个完成再发出对下一个的请求。
相反,我们可以同时触发两个请求,并同时等待。这样优化此功能以在短短一半的时间。
async function getPageData() {const [user, product] = await Promise.all([fetchUser(), fetchProduct()])
}
如果我们想象每个请求都需要 1 秒来响应,而在我们的原始函数中,我们会连续等待两个请求,总共需要 2 秒才能完成,在这个新函数中,我们同时等待两个请求,所以我们函数在 1 秒内完成——时间减半!
缺点
首先,我们在这里根本不处理错误。
所以你可以说“当然,我会把它放在一个大的 try-catch 块中”。
async function getPageData() {try {const [user, product] = await Promise.all([fetchUser(), fetchProduct()])} catch (err) {// 🚩 这会有一个大问题}
}
但这其实有一个大问题。
假设fetchUser
首先完成并出现错误。这将触发我们的 catch 块,然后继续执行该功能。
如果fetchProduct
之后出错,这将不会触发 catch 块。那是因为我们的功能已经继续了。catch 代码已经运行,函数已经完成。
因此,这将导致未处理fetchProduct
的promise.reject
。
解决方法1
解决上述问题的一种方法是将函数传递给.catch()
,例如:
// 用户捕获错误自定义去处理
function handle(err) {alertToUser(err) saveToLoggingService(err)
}
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) // ✅}
}
在这种情况下,如果我们得到一个错误,我们返回处理错误和它本身。所以现在我们的结果user
和product
对象要么是一个Error
,我们可以检查它instanceof
,要么是我们没有报错,正确的结果。
这还不错,解决了我们之前的问题。
但是,这里的主要缺点是我们需要确保我们始终在异步请求后跟着.catch(onReject)
。遗憾的是,这很容易被遗漏,而且也不是最容易为其编写防弹 eslint 规则的方法。
解决方法2
我们并不总是需要在创建Promise
后立即await
。另一种几乎相同的技术是:
**同步请求、异步等待 **
async function getPageData() {// 同时触发两个请求const userPromise = fetchUser().catch(onReject)const productPromise = fetchProduct().catch(onReject)const user = await userPromiseconst product = await productPromise// 处理错误if (user instanceof Error) {handle(user)}if (product instanceof Error) {handle(product)}
}
因为我们在等待任何一个之前触发了每个请求,所以这个版本与我们上面使用Promise.all
.
此外,在这种格式中,try/catch
如果我们愿意,我们可以安全地使用而不会出现我们之前遇到的问题:
async function getPageData() {const userPromise = fetchUser().catch(onReject)const productPromise = fetchProduct().catch(onReject)// Try/catch eachtry {const user = await userPromise} catch (err) {handle(err)}try {const product = await productPromise} catch (err) {handle(err)}
}
在这三者之间,我个人比较喜欢这个Promise.all
版本,因为“这两个东西一起等”感觉更地道。但话虽如此,我认为这只是归结为个人喜好
解决方案 3
Promise.allSettled
Promise.allSettled
JavaScript 中内置的另一种解决方案是使用Promise.allSettled
.
Promise.allSettled
,我们得到一个包含每个承诺结果的值或错误的结果对象。
async function getPageData() {const [userResult, productResult] = await Promise.allSettled([fetchUser(), fetchProduct()])
}
结果对象有 3 个属性:
status
-"fulfilled"
要么"rejected"
value
- 仅在status === 'fulfilled'
时出现。Promise 被resolve
的 valuereason
- 仅在status === 'rejected'
时出现。Promise 被reject
的 reson。
所以我们现在可以读取每个承诺的状态,并单独处理每个错误,而不会丢失任何关键信息:
async function getPageData() {// 同时触发两个请求const [userResult, productResult] = await Promise.allSettled([fetchUser(), fetchProduct()])//userif (userResult.status === 'rejected') {const err = userResult.reasonhandle(err)} else {const user = userResult.value}//productif (productResult.status === 'rejected') {const err = productResult.reasonhandle(err)} else {const product = productResult.value}
}
但是,这是很多重复代码。那么让我们抽象一下:
async function getPageData() {const results = await Promise.allSettled([fetchUser(), fetchProduct()])// 更美观const [user, product] = handleResults(results)
}
我们可以像这样实现一个简单的handleResults
功能:
// 如果发生任何错误 则抛出泛型函数,或返回响应
// 如果没有错误发生
function handleResults(results) {const errors = results.filter(result => result.status === 'rejected').map(result => result.reason)if (errors.length) {// 将所有错误聚合为一个throw new AggregateError(errors)}return results.map(result => result.value)
}
我们可以在这里使用一个巧妙的技巧,即AggergateError类,来抛出一个可能包含多个内部的错误。这样,当被捕获时,我们会通过.errors
an 上的属性获得包含所有详细信息的单个错误AggregateError
,其中包括所有错误:
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)}}
}
后记
项目实际过程中,如果需要捕获每个请求的错误,可以在axios等响应拦截器中判断响应代码是否正确,这样就可以拦截所有错误的请求。 但是如果想在每个页面单独自定义捕获信息就可以用如上方法。 两者并不冲突
总结
- 如果
Promise.all
请求需要捕获每个错误,就用Promise.allSettled
- 确保我们避免混淆——我想指出,重要的是要注意,当我们在这里谈论并发时,我们指的是并发等待Promise,而不是并发执行代码。
- 避免过早优化,并确保在增加更多复杂性之前有充分的理由。速度快固然好,但在盲目并发代码中的所有内容之前考虑是否需要它。
- JavaScript 中的 Promise 非常强大,虽然在过度地将顺序异步/等待转换为并发等待之前应该谨慎行事,但 JavaScript 内置了许多有用的工具,可帮助您在需要时加快速度,这些值得了解。
完全体代码
function handle(err) {alertToUser(err)saveToLoggingService(err)
}
function handleResults(results) {const errors = results.filter(result => result.status === 'rejected').map(result => result.reason)if (errors.length) { throw new AggregateError(errors)}return results.map(result => result.value)
}
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)}}
}
谢谢!
最后
最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。
有需要的小伙伴,可以点击下方卡片领取,无偿分享