返回文章列表

Swift Concurrency 系列 08|我会怎样在真实项目里组织异步代码

真正难的不是某个函数怎么写,而是整条异步链路能不能长期保持清楚

项目里只有一两个异步请求时,很多代码看起来都还不算糟。
真正的分水岭通常出现在下面这些情况同时出现的时候:

  • 同一个页面有首次加载、下拉刷新、重试、筛选切换
  • 本地缓存和远端请求要一起参与
  • 页面离开时要取消一部分任务,保留另一部分任务
  • 多个模块共享某些并发资源

这时你会发现,异步代码最大的难点已经不是“某个函数怎么写”,而是:

整条异步链路怎么组织,才不会越写越散、越写越难改。

这篇文章我不想讲单个 API,而是想讲我在真实项目里更看重的组织原则。

一、我先分的不是函数,而是责任

异步代码一乱,很多人第一反应是“拆函数”。
拆函数当然有用,但如果职责本来就混在一起,拆完通常只是把混乱分成几个小文件。

我更关心的是先把责任分开。通常至少分成三层:

1. 页面层

页面层负责:

  • 触发用户意图
  • 展示页面状态
  • 响应交互变化

它知道“现在应该加载什么”,但不应该负责“怎么编排整条异步流程”。

2. 状态层 / ViewModel

状态层负责:

  • 把用户意图翻译成任务
  • 决定任务之间是并行、替换还是取消
  • 管理 loading、loaded、failed 等页面语义
  • 判断哪些结果还有资格回写页面

它是真正的异步流程收口点。

3. 服务层

服务层负责:

  • 调接口
  • 读缓存
  • 组合多个数据源
  • 提供领域能力

它不应该知道页面长什么样,也不应该偷带 UI 状态语义。

很多异步代码乱掉,不是因为不会写 await,而是这三层一旦混起来,任何需求改动都会同时牵动 UI、流程、状态和数据源。

二、页面层最重要的原则:少知道异步细节

我不喜欢让页面自己去拼太多异步步骤。
因为页面一旦知道太多,就会开始承担这些事:

  • 请求顺序控制
  • 错误兜底
  • 结果过滤
  • 取消策略
  • loading 细分语义

这样改一个需求,往往 UI 和流程一起被牵动。

所以我更倾向于让页面只表达这些事情:

  • “我现在需要刷新”
  • “用户点了重试”
  • “筛选条件变了”

至于背后是:

  • 先读缓存还是先打接口
  • 旧任务要不要停
  • 结果是否已经过期
  • 首次加载和刷新要不要共用同一条链路

尽量都放进状态层收口。

三、页面状态要显式,不要靠一堆零散布尔值拼语义

很多异步页面到后期都会长成这样:

  • isLoading
  • isRefreshing
  • hasError
  • showRetry
  • isEmpty
  • items

这些值单独看都合理,但一组合就容易出现自相矛盾:

  • 既在 loading,又带着旧错误
  • 既显示空态,又保留旧列表
  • 正在刷新,但首次加载标识也还在

所以我更看重“页面语义状态”而不是“很多可自由组合的小状态”。

因为异步页面真正重要的不是“值够不够多”,而是你能不能说清页面现在到底处于什么阶段。

四、任务边界必须清楚,不然一切都只是“先跑起来”

异步结构稳不稳,关键看你能不能回答这些问题:

  • 这个任务是谁拥有的
  • 页面离开时它还要不要继续
  • 新任务来了,旧任务是否立刻失效
  • 这是一个独立任务,还是某条更长流程的一部分

如果这些问题答不清,后面的代码一定会进入一种状态:

  • 每一处看起来都能自圆其说
  • 但拼起来没人能完整复述这条链路到底怎么运作

而一段异步代码如果已经很难被自然语言复述,后面通常也很难被稳定维护。

五、我会尽量把“结果有效性”内建进流程

很多页面乱掉,不是因为结果失败了,而是因为:

  • 旧结果成功返回
  • 但它已经不再对应当前页面上下文

这类问题尤其容易出现在:

  • 搜索
  • 筛选切换
  • 分页
  • 快速进入退出页面

如果结果有效性没有被设计进去,页面就迟早会长出这些奇怪现象:

  • 内容回退
  • loading 莫名结束
  • 错误提示覆盖当前成功状态

所以我很在意:

  • 谁来判断结果是不是过期了
  • 是不是每个调用点都要自己判断
  • 还是在状态层统一收口

我的偏好非常明确:
尽量统一收口,不要让每个视图分支自己判断“这次结果还算不算数”。

六、服务层不要偷偷带页面语义

很多代码乱掉,不是因为服务层不会请求,而是因为服务层开始慢慢掺进页面概念。

比如服务层里开始出现这样的命名或逻辑:

  • “首页首次加载专用请求”
  • “详情页空态兜底逻辑”
  • “这个页面的错误文案拼装”

这些一旦混进去,后面结构就会越来越难复用。
因为服务层开始知道页面长什么样,页面层又开始知道服务层有哪些实现细节,边界很快就糊掉了。

服务层更适合关注的是:

  • 拿到什么数据
  • 如何组合数据源
  • 缓存策略是什么

而不是“这个页面要怎么表现”。

七、我很在意一个原则:异步流程要能被清楚复述

这是我自己做 code review 时很常用的判断标准。

如果拿起一段异步代码,你已经很难用自然语言复述:

  • 入口是什么
  • 主流程是什么
  • 哪些任务会取消
  • 哪些结果会被丢弃
  • 哪些状态由哪一层负责改

那这段代码即使今天能跑,后面也大概率会越来越难演进。

异步代码真正怕的不是多,而是说不清。
一旦说不清,后面任何迭代都是在碰运气。

八、一个更接近落地的组织思路

如果要用一句比较实操的话来总结,我会这样组织:

  • 页面层:表达意图
  • 状态层:收口任务和状态语义
  • 服务层:提供能力
  • 共享资源层:必要时用 Actor 或别的隔离手段管理共享状态

然后再在状态层里明确这些关系:

  • 哪些任务互斥
  • 哪些任务并行
  • 哪些结果可以丢弃
  • 哪些状态必须主 actor 更新

很多人觉得这样“好像多了一层”,但真正复杂项目里,这些层次不是官僚结构,而是为了防止异步逻辑直接蔓延到每个页面事件里。

九、结论:真实项目里的异步组织,核心不是 API,而是边界

真实项目里最重要的,从来不是你会不会 Taskasync letActor
真正决定代码能不能长期演进的,是这些边界有没有清楚:

  • 页面层和流程层的边界
  • 状态层和服务层的边界
  • 共享状态和普通状态的边界
  • 当前有效结果和过期结果的边界

所以如果要用一句话总结这篇文章,我会说:

真实项目里的异步代码组织,关键不在“某个异步 API 怎么写”,而在“任务、状态、结果、职责这四个边界有没有被明确画出来”。

只有这些边界先清楚,异步代码才会从“能跑”真正变成“能长期维护”。