Issue #85

异步请求的顺序,应该由数据依赖决定AI 主写

近 30 天0 次浏览0 位访客统计截至 2026-10-19 · GA4

假设一个页面布局编辑器需要加载两类数据:

  1. 服务端保存的页面详情,例如当前使用双栏布局。
  2. 不同布局对应的预设配置,例如各区域的默认宽度和间距。

新建页面没有历史数据,可以选择一种基础布局完成初始化。为了方便讨论,假设默认使用单栏布局。

编辑页面刚挂载时,服务端详情还没有返回。此时表单为空,于是页面按照新建逻辑,请求了单栏布局的默认配置。

随后,服务端详情返回,表单被回填为双栏布局。到这里,页面状态仍然完全正确。

但先前发出的单栏配置请求仍在等待响应。它随后返回,又把单栏配置写入表单,覆盖了刚刚回填的双栏配置。

两次请求都成功返回了,数据也没有错,为什么页面最后显示的结果却不稳定?

请求顺序不等于生效顺序

理解这个问题,需要区分三种顺序:

  • 发起顺序:哪个请求先发送。
  • 返回顺序:哪个请求的响应先返回。
  • 生效顺序:响应返回后,哪个结果先后写入页面状态。
sequenceDiagram autonumber participant Page as 编辑页面 participant Form as 表单状态 participant Detail as 详情接口 participant Config as 布局配置接口 Page->>Detail: 请求服务端详情 Page->>Form: 读取当前布局 Form-->>Page: 当前为空 Page->>Config: 请求单栏默认配置 Detail-->>Page: 返回已保存的双栏布局 Page->>Form: 写入双栏配置 Note over Form: 当前状态正确 Config-->>Page: 返回单栏默认配置 Page->>Form: 写入单栏配置 Note over Form: 双栏配置被覆盖

这张图中,详情接口先写入双栏配置,布局配置接口随后又写入单栏配置。表单不会判断哪份数据更适合当前页面,只会保留最后一次写入的值。

因此,如果代码在每次响应返回后都直接更新表单,最终结果就取决于哪个响应更晚写入。

一次异步交错是怎样发生的

这里的两个请求访问的是不同接口。详情接口读取已保存的页面,布局配置接口读取单栏布局的默认参数。

它们互不冲突,却都会修改同一个表单:

  • 详情接口返回后,把表单设置为已保存的双栏布局。
  • 布局配置接口返回后,把默认的单栏参数写入表单。

问题就发生在这里。布局配置请求发出时,表单虽然为空,但这并不代表页面没有已保存的布局。

此时详情接口仍在请求中,前端只是暂时不知道布局是什么。只有确认这是新建页面,或者服务端确实没有保存布局,加载默认值的条件才成立。

代码却把“暂时还没拿到布局”当成了“布局为空”,提前请求了单栏默认配置。等到这个请求返回时,详情接口已经把双栏配置写入表单,于是默认值又覆盖了服务端数据。

所以,排查竞态不能只看两个请求谁快谁慢,还要看它们之间是否存在依赖。

请求返回时,发起条件可能已经变化

布局配置接口返回的单栏参数并没有错。问题在于,页面已经不再需要这份参数。

页面请求单栏配置时,代码依据的是这个判断:

表单中的布局字段为空,所以请用单栏配置初始化。

这个判断混淆了两种状态:布局确实为空,以及布局尚未加载完成。等到详情接口返回双栏布局,这个区别才显现出来。

此时单栏配置本身仍然正确,但它从一开始就不适用于这个已有数据的编辑页面。

这也是为什么只查看接口日志很难发现问题。日志会显示两个请求都成功,参数和响应也都正常。真正出错的环节,是响应返回后对表单的写入。

请求成功只说明拿到了结果。这个结果能否用于当前页面,取决于发起它所需的条件是否已经确定。

有依赖的数据,应当按依赖顺序加载

页面详情和布局配置并不是两个互不相关的初始化任务。

布局配置请求需要一个确定的布局,而这个布局来自页面详情。只有详情返回后,程序才能知道应该加载双栏配置,还是在确认没有已保存布局时使用单栏默认配置。

它们的依赖关系是:

页面详情
  → 确定当前布局
    → 加载该布局的配置
      → 写入表单

既然下一步依赖上一步的结果,代码也应该按照这个顺序执行:

async function initializePage() {
  const detail = await fetchPageDetail();
  const layout = detail?.layout ?? DEFAULT_LAYOUT;
  const layoutConfig = await fetchLayoutConfig(layout);

  form.setFieldsValue({
    ...layoutConfig.defaults,
    ...detail,
    layout,
  });
}

这段流程先等待页面详情,再从详情中确定布局,最后请求对应的布局配置。写入时默认配置在前、服务端详情在后,已有数据不会被默认值覆盖。

表单只在这些数据准备完成后统一写入,不需要靠两个请求的返回顺序决定最终结果。

问题代码通常把这条链拆散了:一个 effect 请求页面详情,另一个 effect 发现布局字段为空,便请求默认配置。两个 effect 各自更新表单,却没有表达“布局配置依赖页面详情”这层关系。

多个 effect 本身不是问题。互不依赖的数据可以并行加载;但如果后一个请求的参数或执行条件来自前一个请求,就应该显式串联,而不是让它们从共享状态中猜测对方是否已经完成。

这里也不能简单地把“表单为空”当作使用默认值的条件。编辑页面加载过程中,空值可能只表示详情尚未返回。默认值应该在详情加载完成,并确认没有已保存布局之后才参与初始化。

总结

这个问题表面上是默认值覆盖了服务端数据,实际原因却是代码没有表达数据之间的依赖关系。

页面详情决定布局,布局决定加载哪份配置。只要这条关系被拆到多个独立的异步流程中,最终状态就可能取决于响应返回顺序。

解决问题的重点不是让某个请求更快,也不是调整 effect 的先后,而是让代码中的执行顺序与数据依赖顺序一致:

先拿到依赖的数据
→ 再确定下一步请求
→ 最后统一写入状态

请求发起顺序不等于结果生效顺序。只有把依赖关系写进异步流程,页面状态才不需要依靠最后一次 set 碰运气。

讨论

0 条留言

去 GitHub 留言 ↗

还没有留言,欢迎来打个招呼。

回到顶部