5月31日 15:55

MobX 异步操作为什么要用 runInAction 或 flow?

MobX 处理异步的核心问题只有一个:await 之后,代码已经离开了原来那个 action 的同步执行栈。如果项目开启了 enforceActions,这时直接改 observable 可能报错,调试也会变乱。所以 MobX 异步写法通常有两条路:普通 async/await 搭配 runInAction,或者使用 flow 让 generator 的每一步自动包进 action。

async/await 的常见写法

简单请求用 runInAction 最直观。请求前设置 loading;请求回来之后再改 data、error、loading,就放到 runInAction 里。这样状态变更集中,组件只看到清晰的开始、成功或失败、结束。

javascript
class UserStore { user = null; loading = false; error = null; constructor() { makeAutoObservable(this, {}, { autoBind: true }); } async fetchUser(id) { this.loading = true; this.error = null; try { const user = await api.getUser(id); runInAction(() => { this.user = user; }); } catch (e) { runInAction(() => { this.error = e.message; }); } finally { runInAction(() => { this.loading = false; }); } } }

flow 适合更长的流程

flow 用 generator 写异步,yield 后面的状态修改仍由 MobX 管理。它适合串行步骤多、需要取消任务、错误分支复杂的场景。代价是语法不如 async/await 普及,TypeScript 类型也要多处理一点,团队不熟时会增加理解成本。

javascript
fetchUser = flow(function* (id) { this.loading = true; try { this.user = yield api.getUser(id); } catch (e) { this.error = e.message; } finally { this.loading = false; } });

并行请求和重试都要考虑“最后一次结果才有效”。否则慢请求可能覆盖快请求,页面显示旧数据。常用做法是记录 requestId,或使用 AbortController 取消旧请求。

追问

为什么 @action async 函数里 await 后还要 runInAction?

@action 包住的是函数开始执行的同步部分,不会自动把所有未来的 Promise 回调都包住。await 后恢复执行时,MobX 已经不能保证这段修改仍在 action 中。runInAction 的取舍是多写一点样板代码,换来严格模式下可预测的状态修改。坑在于本地没开 enforceActions 时看不出问题,线上团队规范一收紧就开始报错。

runInAction 和 flow 应该选哪个?

短流程、团队熟悉 async/await 时,用 runInAction 更自然。流程很长、每一步都要改状态,或者希望使用 MobX 自带的取消能力时,flow 更顺手。边界不是哪个更高级,而是代码是否还能一眼看出状态变化发生在哪里。大型团队里最好统一风格,否则同一个项目两套写法会增加维护成本。

loading 状态为什么容易写错?

最常见的坑是并发请求共用一个 boolean。第一个请求还没结束,第二个请求开始;第二个先结束把 loading 设 false,页面就误以为全部结束了。可以用 requestId 保证只处理最后一次请求,或用 pendingCount 表示还有几个请求未完成。取舍是代码稍复杂,但能避免旧请求覆盖新结果和 loading 闪烁。

javascript
const id = ++this.requestId; const data = await api.search(keyword); runInAction(() => { if (id === this.requestId) this.results = data; });

异步错误应该放在 store 里还是组件里?

业务错误通常放 store 里,因为多个组件可能都要根据它展示状态或禁用按钮。只影响某个弹窗的一次性错误,可以留在组件本地,没必要污染全局 store。边界是错误是否属于业务状态:比如“用户未登录”是业务状态,“当前弹窗输入为空”更像 UI 状态。踩坑点是把所有 error 都塞进全局数组,最后用户看到的提示和真实操作对不上。

组件卸载时异步任务要不要取消?

如果请求结果只会写入全局 store,组件卸载不一定必须取消,但要防止过期结果覆盖新状态。若结果只服务当前组件,卸载时取消更稳,尤其是搜索框、详情页切换和轮询。flow 返回的任务可以 cancel,fetch 可以用 AbortController。边界在于:取消不是为了让 MobX 安全,而是为了避免无意义请求和过期数据写回。

标签:Mobx