
不要再这样编写 async/await
最开始接触 async/await 时,很多人都会发出“终于有这个功能了!”的感叹。它的语法清晰、可读性强,用起来直观又顺手。
然而,用得越久,就会发现一些常见的“坑”时常在各种项目里出现:有些是代码审查时发现的,有些是和同事讨论时暴露的问题。这些都说明异步编程本质上并不简单。
下文就结合实际经验,列出了一些常见的异步陷阱,以及更高级的用法与思考方式,让代码更健壮,也更易维护。
从回调地狱到 async/await
还记得当初的回调地狱吗?JavaScript 进化到现在,已经让我们避免了深层嵌套的回调结构。
但功能变强大了,责任也跟着变大。下面是 async/await 中常见的三大“致命罪状”。
1. 同步式的瀑布请求
糟糕示例:顺序等待
在代码审查里经常看到这样的场景:本来可以并发执行的请求,却被一个接一个地串行处理。
如果每个请求都要 200ms,这里就总共要 600ms,给用户的体验自然不佳。
改进:并发执行
对于相互独立的操作,应该使用并发来节省时间:
同样是获取用户、帖子和通知,响应速度立刻加快三倍左右。不过,并发并非万能。
依赖关系:如果一个请求需要另一个请求返回的数据,就必须顺序执行。
竞态条件:如果多个请求会同时修改某个共享资源,可能导致数据不一致。
资源限制:并发过多会给服务器或浏览器带来压力。
举例:危险的并发
如果并行更新同一个用户资料,服务器可能会出现覆盖数据的情况。必须根据业务逻辑判断能否并发执行。
2. 隐形错误:如何平衡异常处理
常见误区:把错误直接“吞掉”
不少人喜欢在 catch
块里写个简单的 console.error(error)
,然后返回 null
,让外层调用时貌似一切正常。
看似“处理”了错误,但实际上把错误原因都藏起来了。网络断了?JSON 解析失败?服务器返回 500?外部代码只能拿到 null
,毫无头绪。
更好的做法:区分场景处理
返回空值并非一直不对。如果它只是一个不关键的功能,比如推荐列表或活动通知,给用户一个空状态也许是更友好的方式。但如果是核心数据,就应该抛出异常或者做更明确的错误处理,让上层逻辑感知到问题。
高级开发者通常会这样写:
这样就能同时兼顾稳定性和可维护性。关键数据绝不能“悄悄失败”,而次要功能可以“优雅退化”。
3. 内存泄漏的陷阱和现代化的清理方式
典型误区:无休止的轮询
假设写了一个定时轮询,几秒钟拉取一次数据:
表面看上去没什么问题,但这样会导致:
组件或页面卸载后依然在轮询
如果
fetchData()
执行得很慢,可能会同时发起多次请求更新 UI 时,目标 DOM 甚至可能已经被移除
改进:AbortController + 轮询管理
下面这个示例借助 AbortController
实现了更安全的轮询:
通过使用 AbortController
,可以在需要时终止请求并及时释放资源,更好地控制组件的生命周期和内存占用。
高级开发者的工具箱
1. 重试(Retry)模式
网络环境不稳定或第三方服务时好时坏的情况下,只尝试一次就放弃不是好办法。可以加上重试和退避策略:
除了基本的指数退避,还可以考虑:
避免过度重试导致资源浪费
区分哪些错误类型才需要重试
使用断路器(Circuit Breaker)模式保护系统
加入随机抖动(Jitter)防止大量请求同时重试
2. 资源管理
启动异步操作简单,关键是如何“优雅地”停止它们。通过统一的 ResourceManager
或类似模式,可以集中处理一些关闭连接、清理定时器、取消任务等逻辑:
真实场景中的模式
1. 统一的加载状态管理
不要在每个组件都写一堆 “正在加载”、“错误” 判断。可以抽象出一个自定义 Hook 或者统一的加载管理逻辑:
这样可以:
保持加载和错误状态处理的一致性
便于集中管理和优化
在组件卸载时自动清理
2. 数据同步器(Data Synchronizer)
对于实时性要求高的应用,与其一个个写请求,不如建立一个数据同步管理器,统一处理轮询/订阅/数据合并等逻辑:
几条核心原则
并发原则
让互不依赖的操作同时执行
小心竞态和依赖顺序
不要为了并发而并发,要考虑业务逻辑
弹性原则(Resilience)
及时重试和错误处理
做好网络或服务不可靠的准备
避免一个错误拖垮整个系统
资源管理原则
主动清理异步操作
终止不再需要的请求
防止内存泄漏
用户体验原则
有意义的加载提示和错误信息
保证核心功能的可用性
在可能的情况下提供取消或中断操作
常见问题
1. 什么时候用 Promise.all
,什么时候用 Promise.allSettled
?
Promise.all
适合所有请求都必须成功的场景Promise.allSettled
允许部分失败,适合容忍部分请求出错的需求Promise.race
则常用于超时等情况
2. 在 React 中如何优雅地清理?
善用
useEffect
的返回值进行清理使用
AbortController
终止 HTTP 请求组件卸载时,保证所有定时器、订阅或轮询都能停止
展望
检查代码:查找本可并发却写成串行的请求;检查错误处理是否含糊不清;关注异步操作的清理是否充分。
改进模式:引入更健壮的错误处理,增加重试逻辑,优化资源释放。
考虑扩展性:当用户量或请求量激增时,如何保证依旧能流畅运行?如果某些服务变慢甚至挂掉,该如何部分降级?
为什么要在意这些细节
或许有人会说,“我的小项目没这么复杂,用不着搞这些”。但真正的好代码是能经得住放大和演进的。
可扩展性:面对更多用户和请求,系统能否稳健地运行
用户体验:高并发、良好错误处理能让应用体验更流畅
开发者体验:清晰的异步逻辑有助于日后维护和团队协作
资源利用:合理的并发和清理机制能节约服务器和客户端资源
这些并不是纸上谈兵,而是大量实战总结出来的硬道理。随着项目的规模和复杂度不断提升,这些异步编程模式会是你写出高质量前端代码的核心基石。