在现代JavaScript异步编程中,Promise.all无疑是处理并发任务的利器。然而,当任务之间存在复杂的依赖关系时,Promise.all的局限性便会显现,开发者往往陷入手动优化和维护"依赖地狱"的困境。今天,我们将深入探讨一个名为better-all的库,它如何通过巧妙的设计,优雅地解决了这一痛点,并带来了更高效、更可读的异步任务管理方式。
Promise.all 的困境:当依赖遇上并发
Promise.all的设计初衷是并行执行一组独立的Promise,并在所有Promise都解决后返回结果。但当任务存在依赖时,事情就变得棘手了。让我们通过几个场景来理解Promise.all在处理依赖时的不足。
场景一:朴素的串行执行(低效)
假设我们有三个异步函数 getA(), getB(), getC(a),其中 getC 依赖于 getA 的结果。如果按照最直观的方式编写代码,可能会是这样:
// 假设 getA 耗时 1s, getB 耗时 10s, getC 耗时 10s
const [a, b] = await Promise.all([getA(), getB()]) // a: 1s, b: 10s → 此步耗时 10s
const c = await getC(a) // c: 10s → 此步耗时 10s
// 总耗时约 20 秒
在这个例子中,getA() 和 getB() 并行执行,耗时取决于较慢的 getB() (10s)。然后 getC(a) 串行执行,又耗时 10s。总耗时高达 20 秒。
场景二:手动优化(复杂且脆弱)
为了优化上述情况,我们可能会尝试手动调整 Promise 的组合方式,让 getB() 和 getC(a) 并行:
// 假设 getA 耗时 1s, getB 耗时 10s, getC 耗时 10s
const a = await getA() // a: 1s -> 此步耗时 1s
const [b, c] = await Promise.all([
// b: 10s, c: 10s -> 此步耗时 10s
getB(),
getC(a),
])
// 总耗时约 11 秒
这次优化后,总耗时降到了 11 秒,因为 getA() 完成后,getB() 和 getC(a) 可以并行。看起来不错,但问题来了:如果任务的耗时发生变化呢?
场景三:手动优化的反噬(难以维护)
假设现在 getA() 耗时 10 秒,而 getC() 耗时 1 秒。我们再来看看手动优化后的代码表现:
// 假设 getA 耗时 10s, getB 耗时 10s, getC 耗时 1s
const a = await getA() // a: 10s -> 此步耗时 10s
const [b, c] = await Promise.all([
// b: 10s, c: 1s -> 此步耗时 10s
getB(),
getC(a),
])
// 总耗时约 20 秒
此时,总耗时又回到了 20 秒!而最初的朴素写法(场景一)在同样的情况下,总耗时却是 11 秒:
// 朴素写法在相同耗时下的表现:
// 假设 getA 耗时 10s, getB 耗时 10s, getC 耗时 1s
const [a, b] = await Promise.all([getA(), getB()]) // a: 10s, b: 10s → 此步耗时 10s
const c = await getC(a) // c: 1s → 此步耗时 1s
// 总耗时约 11 秒
这说明手动优化不仅复杂,而且非常脆弱,难以适应任务耗时的动态变化。在真实世界中,任务的耗时往往受网络、服务器负载等多种因素影响,是不可预测的。为了正确优化,你甚至需要手动分析并声明复杂的依赖图,这会迅速导致代码难以管理和阅读:
const [[a, c], b] = await Promise.all([getA().then((a) => getC(a).then((c) => [a, c])), getB()])
better-all 的解决方案:自动依赖优化与类型推断
better-all库的核心思想是提供一个增强版的all函数,它允许开发者以声明式的方式定义异步任务及其依赖,而无需手动管理Promise链或复杂的嵌套。它会自动分析任务间的依赖关系,并尽可能地并行执行任务,从而实现最大化的并发优化。
让我们看看better-all如何优雅地解决上述问题:
import { all } from 'better-all'
const { a, b, c } = await all({
async a() {
return getA()
}, // 假设耗时 1s
async b() {
return getB()
}, // 假设耗时 10s
async c() {
return getC(await this.$.a)
}, // 假设耗时 10s,依赖任务 a
})
// 总耗时约 11 秒 - 实现了最优并行化!
在这个例子中,better-all会自动识别出任务c依赖于任务a。它会立即启动所有任务,当任务c执行到await this.$.a时,如果任务a尚未完成,它会等待a完成;同时,任务b会独立并行执行。这样,better-all在保证依赖顺序的同时,最大限度地提升了并行度,无论任务耗时如何变化,都能保持最优或接近最优的执行效率。
better-all的"魔法"在于其this.$对象,它让你能够以自然的方式访问其他任务的结果(作为Promise),从而清晰地表达依赖关系。库会确保所有任务尽早启动,并在遇到依赖时智能等待。
更多应用场景
带有依赖的任务:
const { user, profile, settings } = await all({
async user() {
return fetchUser(1)
},
async profile() {
return fetchProfile((await this.$.user).id)
},
async settings() {
return fetchSettings((await this.$.user).id)
},
})
// user 任务首先运行,然后 profile 和 settings 任务并行运行
复杂的依赖图:
const { a, b, c, d, e } = await all({
async a() {
return 1
},
async b() {
return 2
},
async c() {
return (await this.$.a) + 10
},
async d() {
return (await this.$.b) + 20
},
async e() {
return (await this.$.c) + (await this.$.d)
},
})
// a 和 b 并行运行
// c 等待 a,d 等待 b (c 和 d 可以重叠并行)
// e 等待 c 和 d 都完成
// 结果: { a: 1, b: 2, c: 11, d: 22, e: 33 }
console.log({ a, b, c, d, e })
核心实现机制:Proxy与智能调度
better-all之所以能实现如此优雅的依赖管理,其内部机制主要依赖于JavaScript的Proxy对象和一套精巧的Promise延迟解决(Deferred Resolution)逻辑。
1. this.$的Proxy代理
better-all为每个任务函数提供了一个特殊的this.$上下文。这个$对象实际上是一个Proxy。当你在任务函数中通过await this.$.dependencyName访问某个依赖时,Proxy会拦截这个访问。
2. 智能等待机制
当Proxy拦截到对this.$.dependencyName的访问时,它会检查dependencyName对应的任务是否已经完成。如果已完成,它会立即返回结果;如果尚未完成,它会创建一个新的Promise,并将当前任务的resolve和reject函数注册到dependencyName任务的等待队列中。
3. 任务的并行启动与结果管理
better-all在开始时会立即启动所有定义的异步任务。每个任务的结果(无论是成功解决还是失败拒绝)都会被存储起来。当一个任务完成时,它会遍历所有等待它的依赖任务,并触发它们的resolve或reject函数,从而解除这些依赖任务的阻塞。
4. 强大的类型推断
得益于TypeScript的强大能力,better-all在设计时充分利用了泛型和Awaited<R>等高级类型特性。这意味着,无论你定义了多么复杂的任务和依赖关系,this.$.dependencyName都能提供精确的类型信息,并且最终返回的结果对象也是完全类型安全的,极大地提升了开发体验和代码健壮性。
better-all 的优势总结
- • 自动最大化并行度:无需手动分析和优化依赖图,库会自动处理,确保任务在满足依赖的前提下尽可能并行执行。
- • 极佳的可读性:通过
await this.$.dependency的语法,以最直观的方式表达任务依赖,代码逻辑清晰明了。 - • 全方位类型安全:借助TypeScript,从任务定义到结果获取,全程提供精确的类型推断,有效避免潜在的类型错误。
- • 轻量级:库本身依赖少,打包体积小,对项目性能影响微乎其微。
- • 错误处理:错误传播机制与
Promise.all类似,一个任务失败会导致依赖它的任务也失败,并通过try...catch捕获。
适用场景与使用建议
✅ 适合使用的场景
better-all特别适用于以下场景:
- 数据聚合:需要从多个API或数据库中获取数据,且部分数据获取存在前后依赖。
- 复杂计算流:一系列计算步骤,其中某些步骤的结果是后续步骤的输入。
- 构建流程:例如在构建系统中,某些模块的编译依赖于其他模块的输出。
⚠️ 不适合使用的场景
- 任务完全独立:如果所有任务都是独立的,直接使用
Promise.all更简单。 - 动态依赖:如果依赖关系只能在运行时确定,
better-all的静态依赖声明方式可能不适用。 - 需要精细控制并发数:如果需要限制同时执行的任务数量,可能需要配合其他库如
p-limit。
💡 最佳实践
// ✅ 推荐:清晰的任务命名
const result = await all({
async userData() {
return fetchUser(userId)
},
async userProfile() {
const user = await this.$.userData
return fetchProfile(user.id)
}
})
// ❌ 避免:过度复杂的依赖图(超过3层)
// 如果依赖层级太深,考虑拆分或重构
结语
better-all提供了一种更高级、更智能的异步任务编排方式,它将开发者从繁琐的依赖管理中解放出来,让我们能够更专注于业务逻辑本身。在追求高性能和高可维护性的现代前端开发中,better-all无疑是一个值得关注和尝试的优秀工具。它不仅提升了开发效率,也让我们的异步代码更加健壮和优雅。
记住:好的工具不是让简单的事情变复杂,而是让复杂的事情变简单。
💡 React Hooks 工具库推荐 > @reactuses/core - 100+ 个生产级 Hooks,覆盖状态、副作用、浏览器 API 等场景 > 📖 reactuse.com | 📦
npm i @reactuses/core