Promise.allSettled() 和 Promise.any() 有什么问题?
Promise.allSettled
Promise.any
不一致
命名
接受拒绝
结论
我最近在 v8 博客上读了一篇关于Promise 组合器的文章。文章讲的是 Promise API 中即将推出的两个方法:Promise.allSettled()
和Promise.any()
。我感到很沮丧。这些方法的设计在我看来与当前的 Promise API 不一致。下面我来分享一下我的看法。
Promise.allSettled
根据文章:
Promise.allSettled
当所有输入的承诺都得到解决时,会向您发出信号,这意味着它们要么被实现,要么被拒绝。
用例是发送几个 API 调用并等待全部完成:
const promises = [
fetch('/api-call-1'),
fetch('/api-call-2'),
fetch('/api-call-3'),
];
await Promise.allSettled(promises);
removeLoadingIndicator();
当然,这很有用。但这个任务可以用 .map()
和轻松解决Promise.all()
。改动很小:
const promises = [
fetch('/api-call-1'),
fetch('/api-call-2'),
fetch('/api-call-3'),
].map(p => p.catch(e => e)); // <-- the only change
await Promise.all(promises);
removeLoadingIndicator();
是否值得实现一个只需几行代码就能解决的新核心方法?对我来说,这是一个库级功能,而不是核心 API 方法。
但更重要的是,它Promise.allSettled
带来了额外的抽象,增加了代码复杂度。与此不同,Promise.all
它使用包装对象数组{status, reason}
而不是纯 Promise 值来实现。作为一名开发者,我不喜欢它。我期望名称相似的方法 .all()/.allSettled()
能够以类似的方式运行。但事实并非如此。
此外,这种代码Promise.allSettled
会导致更糟糕的错误控制。错误应该从最终结果中过滤掉,而不是像传统那样在 catch 块中处理。这反过来又有以下缺点:
- 错误发生时不会立即处理。如果出现多个相关错误,您无法确定哪个是原始错误。日志中会包含错误的时间戳。
- 如果至少有一个承诺永远处于待决状态,则不会处理错误。
目前的方法Promise.all
不允许这样的事情发生。
Promise.any
Promise.any
一旦其中一个承诺实现,就会向您发出信号。
换句话说Promise.any
就是Promise.race
忽略拒绝。
用例是检查多个端点并从第一个成功的端点获取数据:
const promises = [
fetch('/endpoint-a').then(() => 'a'),
fetch('/endpoint-b').then(() => 'b'),
fetch('/endpoint-c').then(() => 'c'),
];
try {
const first = await Promise.any(promises);
} catch (error) {
// All of the promises were rejected.
console.log(error);
}
我同意有时它可能有用。但频率是多少?在多少个项目中,你使用过这种模式,向相同的端点发出多个并行请求来获取相同的数据?欢迎在评论中分享。但在我看来,这种情况并不常见。如果社区能获得Bluebird 的 原生实现,岂不是更有用Promise.each()
?Promise.delay()
此外,Promise.any
还引入了一种新的错误类型——错误AggregateError
。如果所有 Promise 都被拒绝,此类错误会包含指向其他错误的链接。这又是一种错误处理方式!它不同于从Promise.allSettled
成功结果中提取错误的方式,也不同于Promise.all/Promise.race
仅使用Error
实例的拒绝方式。如果每个新的 Promise API 方法都引入新的错误处理方式,JavaScript 将会变成什么样?虽然该提案尚处于非常早期的阶段,但我对其发展方向仍心存疑虑。
基于当前的 Promise API,实现Promise.any
有点棘手,但实际上只有两行代码:
const reverse = p => new Promise((resolve, reject) => Promise.resolve(p).then(reject, resolve));
Promise.any = arr => reverse(Promise.all(arr.map(reverse)));
我们难道不应该把它留在库中并保持核心 Promise API 干净、简单吗?
不一致
为啥Promise.all
和Promise.race
这么漂亮?
因为它们的行为非常一致,与普通的 Promise 类似:仅用一个值即可完成,仅用一个错误即可拒绝。无需包装值,无需累积错误,也无需额外的复杂性。
为什么Promise.allSettled
和Promise.any
对我来说这么奇怪?
Promise.allSettled
执行时返回一个包含状态和原因的对象数组,其中包含底层的 Promise 值。拒绝时……永远不会。Promise.any
使用单一值完成,并忽略中间的拒绝。只有当所有 Promise 都被拒绝时,它才会使用包含所有底层原因的累积原因来拒绝。
这些新方法确实很难理解,因为它们与现有的 Promise API 有很大不同。
我预测2020年一个热门的求职面试问题:
这四种方法有什么区别?
Promise.all()
Promise.allSettled()
Promise.race()
Promise.any()
虽然这是一个很酷的问题,但我不认为核心 API 应该鼓励这种复杂性。
命名
我对命名也感到失望。四个行为略有不同的方法应该有一个非常清晰的名称。否则,每次在代码中遇到它们,我都得重新检查MDN 。以下是提案Promise.any
:
它清楚地描述了它的作用
我不同意。对我来说,这个名字Promise.any
让人困惑:
- 如果任何一个承诺实现,它也会实现吗?是的。
- 如果任何一个Promise 被拒绝,它也会拒绝吗?不会。
- 如果任何承诺达成,它是否也会达成?这要视情况而定。
- 它和 有什么不同
Promise.race
?嗯。
我认为,每个方法的名称都应该明确定义该方法满足的条件。我建议采用以下命名约定:
Promise.all -> Promise.allFulfilled
Promise.allSettled -> Promise.allSettled
Promise.race -> Promise.oneSettled
Promise.any -> Promise.oneFulfilled
它反映了 Promise 状态的四种可能组合。它解释了为什么这些方法在提案中被称为组合器
。 当然,这种重命名是无法实现的Promise.all
,因为Promise.race
许多应用程序已经实现了并投入使用。但对于新方法来说,制定一些命名策略会非常有帮助。
我已经在 GitHub 上的提案存储库中打开了问题Promise.any()
,欢迎您分享您的想法。
接受拒绝
总的来说,我对新方法中引入的未抛出的“吞掉”拒绝的概念不太感冒。事实上,新的 Promise API 提供了一种静默忽略代码中错误的方法:
Promise.allSettled
从不拒绝。Promise.any
仅当所有承诺都被拒绝时才拒绝。
目前没有其他核心 JavaScript API 可以做到这一点。忽略错误的唯一方法是手动将其包装成try..catch / .catch()
空的主体。并在此处写上忽略错误的原因注释,否则eslint 会发出警告。
我认为核心 API 应该暴露所有错误。是否处理错误始终由开发者决定。这应该对其他开发者清晰地展示。试想一下,如果错误地使用了吞噬拒绝,会耗费多少时间进行调试!尤其是在处理第三方代码时——某些代码无法正常工作,却没有抛出任何错误。
结论
我每天都会用到 Promise,就像许多其他开发者一样。我喜欢 JavaScript 的异步特性。清晰直观的 API 让我能够更快地完成任务,提高效率。因此,我认为 Promise API 应该被谨慎对待和修改。
感谢您的阅读,欢迎留言评论。
这篇文章最先出现在hackernoon.com上。
文章来源:https://dev.to/vitalets/what-s-wrong-with-promise-allsettled-and-promise-any-5e6o