Swift Concurrency 系列 08|我会怎样在真实项目里组织异步代码
真正难的不是某个函数怎么写,而是整条异步链路能不能长期保持清楚
项目里只有一两个异步请求时,很多代码看起来都还不算糟。
真正的分水岭通常出现在下面这些情况同时出现的时候:
- 同一个页面有首次加载、下拉刷新、重试、筛选切换
- 本地缓存和远端请求要一起参与
- 页面离开时要取消一部分任务,保留另一部分任务
- 多个模块共享某些并发资源
这时你会发现,异步代码最大的难点已经不是“某个函数怎么写”,而是:
整条异步链路怎么组织,才不会越写越散、越写越难改。
这篇文章我不想讲单个 API,而是想讲我在真实项目里更看重的组织原则。
一、我先分的不是函数,而是责任
异步代码一乱,很多人第一反应是“拆函数”。
拆函数当然有用,但如果职责本来就混在一起,拆完通常只是把混乱分成几个小文件。
我更关心的是先把责任分开。通常至少分成三层:
1. 页面层
页面层负责:
- 触发用户意图
- 展示页面状态
- 响应交互变化
它知道“现在应该加载什么”,但不应该负责“怎么编排整条异步流程”。
2. 状态层 / ViewModel
状态层负责:
- 把用户意图翻译成任务
- 决定任务之间是并行、替换还是取消
- 管理 loading、loaded、failed 等页面语义
- 判断哪些结果还有资格回写页面
它是真正的异步流程收口点。
3. 服务层
服务层负责:
- 调接口
- 读缓存
- 组合多个数据源
- 提供领域能力
它不应该知道页面长什么样,也不应该偷带 UI 状态语义。
很多异步代码乱掉,不是因为不会写 await,而是这三层一旦混起来,任何需求改动都会同时牵动 UI、流程、状态和数据源。
二、页面层最重要的原则:少知道异步细节
我不喜欢让页面自己去拼太多异步步骤。
因为页面一旦知道太多,就会开始承担这些事:
- 请求顺序控制
- 错误兜底
- 结果过滤
- 取消策略
- loading 细分语义
这样改一个需求,往往 UI 和流程一起被牵动。
所以我更倾向于让页面只表达这些事情:
- “我现在需要刷新”
- “用户点了重试”
- “筛选条件变了”
至于背后是:
- 先读缓存还是先打接口
- 旧任务要不要停
- 结果是否已经过期
- 首次加载和刷新要不要共用同一条链路
尽量都放进状态层收口。
三、页面状态要显式,不要靠一堆零散布尔值拼语义
很多异步页面到后期都会长成这样:
isLoadingisRefreshinghasErrorshowRetryisEmptyitems
这些值单独看都合理,但一组合就容易出现自相矛盾:
- 既在 loading,又带着旧错误
- 既显示空态,又保留旧列表
- 正在刷新,但首次加载标识也还在
所以我更看重“页面语义状态”而不是“很多可自由组合的小状态”。
因为异步页面真正重要的不是“值够不够多”,而是你能不能说清页面现在到底处于什么阶段。
四、任务边界必须清楚,不然一切都只是“先跑起来”
异步结构稳不稳,关键看你能不能回答这些问题:
- 这个任务是谁拥有的
- 页面离开时它还要不要继续
- 新任务来了,旧任务是否立刻失效
- 这是一个独立任务,还是某条更长流程的一部分
如果这些问题答不清,后面的代码一定会进入一种状态:
- 每一处看起来都能自圆其说
- 但拼起来没人能完整复述这条链路到底怎么运作
而一段异步代码如果已经很难被自然语言复述,后面通常也很难被稳定维护。
五、我会尽量把“结果有效性”内建进流程
很多页面乱掉,不是因为结果失败了,而是因为:
- 旧结果成功返回
- 但它已经不再对应当前页面上下文
这类问题尤其容易出现在:
- 搜索
- 筛选切换
- 分页
- 快速进入退出页面
如果结果有效性没有被设计进去,页面就迟早会长出这些奇怪现象:
- 内容回退
- loading 莫名结束
- 错误提示覆盖当前成功状态
所以我很在意:
- 谁来判断结果是不是过期了
- 是不是每个调用点都要自己判断
- 还是在状态层统一收口
我的偏好非常明确:
尽量统一收口,不要让每个视图分支自己判断“这次结果还算不算数”。
六、服务层不要偷偷带页面语义
很多代码乱掉,不是因为服务层不会请求,而是因为服务层开始慢慢掺进页面概念。
比如服务层里开始出现这样的命名或逻辑:
- “首页首次加载专用请求”
- “详情页空态兜底逻辑”
- “这个页面的错误文案拼装”
这些一旦混进去,后面结构就会越来越难复用。
因为服务层开始知道页面长什么样,页面层又开始知道服务层有哪些实现细节,边界很快就糊掉了。
服务层更适合关注的是:
- 拿到什么数据
- 如何组合数据源
- 缓存策略是什么
而不是“这个页面要怎么表现”。
七、我很在意一个原则:异步流程要能被清楚复述
这是我自己做 code review 时很常用的判断标准。
如果拿起一段异步代码,你已经很难用自然语言复述:
- 入口是什么
- 主流程是什么
- 哪些任务会取消
- 哪些结果会被丢弃
- 哪些状态由哪一层负责改
那这段代码即使今天能跑,后面也大概率会越来越难演进。
异步代码真正怕的不是多,而是说不清。
一旦说不清,后面任何迭代都是在碰运气。
八、一个更接近落地的组织思路
如果要用一句比较实操的话来总结,我会这样组织:
- 页面层:表达意图
- 状态层:收口任务和状态语义
- 服务层:提供能力
- 共享资源层:必要时用 Actor 或别的隔离手段管理共享状态
然后再在状态层里明确这些关系:
- 哪些任务互斥
- 哪些任务并行
- 哪些结果可以丢弃
- 哪些状态必须主 actor 更新
很多人觉得这样“好像多了一层”,但真正复杂项目里,这些层次不是官僚结构,而是为了防止异步逻辑直接蔓延到每个页面事件里。
九、结论:真实项目里的异步组织,核心不是 API,而是边界
真实项目里最重要的,从来不是你会不会 Task、async let 或 Actor。
真正决定代码能不能长期演进的,是这些边界有没有清楚:
- 页面层和流程层的边界
- 状态层和服务层的边界
- 共享状态和普通状态的边界
- 当前有效结果和过期结果的边界
所以如果要用一句话总结这篇文章,我会说:
真实项目里的异步代码组织,关键不在“某个异步 API 怎么写”,而在“任务、状态、结果、职责这四个边界有没有被明确画出来”。
只有这些边界先清楚,异步代码才会从“能跑”真正变成“能长期维护”。