

写前端绕不开异步。最常见的方式,是用 Promise
配合 async/await
,再加上一点 useEffect
。单个任务时,这些工具简洁又直观。但一旦逻辑开始交织,表面上看似轻巧的代码,很容易变成一张缠绕的网。
这一点,在做一个在线编辑器时感受尤深。
需求的模样#
编辑器的需求并不复杂,却带着一些耐人寻味的细节:
- 用户输入时不要急着保存,等他停下 800ms 再说。
- 如果上一次的保存还没完成,新的输入就来了,那就以新的为准,旧的取消掉。
- 网络偶尔不稳时,要静默地重试三次,每次隔两秒。
- 界面上需要有反馈,告诉用户现在是“正在保存…”、“已保存”,还是“失败了”。
- 如果三次重试都失败,就该给出明确的“保存失败”提示。
- 关键节点还需要记录日志。
这些规则凑在一起,不算宏大,但已经能感受到一种细微的复杂:它们不是单点的,而是连锁的,像是珠子被一根细线串起来。
初始的写法:碎片的累积#
第一反应自然是用 useEffect
。
- 防抖要
setTimeout
与clearTimeout
。 - 取消请求要
AbortController
。 - 重试逻辑要在
catch
里记录次数,再套定时器。 - 界面状态则要靠一堆
useState
来驱动。
代码能跑,但随着逻辑的累积,状态和副作用散落在不同角落。一个状态触发另一个 useEffect
,后者又会更新别的状态。表面看是模块化,实则是线头交织。过段时间再回头,要重新理清逻辑链,就像拆一团毛线,常常一拉就乱。
尤其“取消”与“重试”叠在一起时,更是容易出 bug。那一刻,我开始觉得:也许问题不是写法不够熟练,而是思路本身需要调整。
换一个角度:点与流#
我们习惯把每个异步任务看作孤立的点,然后用 if/else、状态标志、清理函数,把这些点强行连起来。但用户输入、网络返回,本质并不是点,而是一连串的事件序列。
与其在点之间搭桥,不如承认这就是一条流。
于是,想到了 RxJS。
它的思路很直接:把输入看作数据流,交给操作符处理,输出我们想要的结果。这样就不再需要到处维护标志位和清理逻辑,而是像搭建一条管道,把水流顺势引导。
实现的雏形#
先准备日志服务,用 React Context 提供。
再在自定义 Hook 里,搭建一个声明式的管道。所有逻辑——防抖、取消、重试、状态更新——都在这条管道上衔接。
核心代码大致如下:
// hooks/useDocumentAutoSaveRxJS.ts
// ...
const autoSavePipe$ = content$.pipe(
debounceTime(800),
distinctUntilChanged(),
tap(() => {
logger.log('准备保存...');
setSaveStatus('saving');
}),
switchMap(currentContent =>
from(fakeApiSave(currentContent)).pipe(
tap(() => {
logger.log(`内容 "${currentContent}" 保存成功。`);
}),
retry({
count: 3,
delay: (error, retryCount) => {
logger.warn(`保存失败,第 ${retryCount} 次重试...`, error);
return timer(2000);
}
}),
catchError(error => {
logger.error('所有重试均失败。', error);
return of({ status: 'error', error });
})
)
)
);
tsx这里 switchMap
会自动取消上一次未完成的请求,retry
则优雅地处理失败后的重试,catchError
保证管道不会因为错误而崩溃。
代码聚拢在一个地方,逻辑的走向清晰许多。以前需要东拼西凑的,现在能在一条流里自然展开。
技术之外的思考#
写代码时常常会遇到类似的困境:一开始问题是简单的,后来细节叠加,逻辑互相牵扯,最终难以收拾。
在这种时候,强行在原有的框架里缝缝补补,往往越补越乱。与其如此,不如退后一步,重新看清问题的本质:我们究竟在处理什么?
就像这次,问题表面是“如何写防抖、取消、重试”,但本质其实是“如何描述一条连续的事件流”。当视角一旦转换,解决方式就自然浮现。
这种经验不仅限于前端。很多工作——无论是写程序,还是与人合作,抑或是生活里的选择——都存在类似的规律。事情一旦缠绕,不妨停下来,问问自己:眼前的是不是一条流?是不是有另一种方式去看待?
结语#
async/await
很好,它让单个异步任务变得清晰。
但当面对的是一串相互关联、有时间顺序、可能被中断或需要重试的事件时,仅靠 async/await
就会显得吃力。那时候,我们常常在用大量的状态和清理逻辑,去模拟一个流。
而 RxJS 让我们可以直接描述流。它并不是多么神奇的工具,而是提供了一种更自然的视角,让复杂重新变得清晰。
所以,当你下次写代码时,若发现逻辑渐渐纠缠,不妨先放下手里的补丁。想一想,你面对的,也许不是散乱的点,而是一条流。