很多人把异步竞态看成代码组织问题,实际它首先是正确性问题。市集摊位名输入时的远端重名提示一旦出现“先输入“晨光”,再改成“晨光手作”,第一个慢响应反而覆盖第二次结果,页面把合法名称标成重复”的路径,用户看到的状态就可能与系统最终处理的内容不同。

技术主题封面

观察到的现象

异步校验的典型失败不是接口不能调用,而是返回先后顺序不可预测。用户键入“晨光”,网络请求 A 发出;随后改成“晨光手作”,请求 B 先返回可用。几秒后 A 才返回重复,如果不做版本判断,界面就把最新名称标为不可用。

“远端响应存在不确定顺序,必须以输入版本而非请求完成时刻决定结果归属”是这篇最重要的设计判断。单看成功路径,市集摊位名输入时的远端重名提示可以靠若干事件回调工作;但只要用户中途修改、取消或重复操作,若没有清晰约束,就无法确定最终输出究竟属于哪一次输入。工程拆分必须以这些不确定性为起点。

我们希望得到的不是一张孤立的效果图,而是一条可追溯的处理链。整个过程拆为输入变化、token 递增、发起校验、比较版本、接受或丢弃。每一段只承诺自己负责的结果,下一段不靠猜测前一步的内部状态。对异步竞态而言,这种清晰分工也是回归测试能够写得具体的前提。

不要尝试用“请求创建时间”推导当前输入值。客户端给每次输入变化分配递增版本号,校验完成时检查版本仍相同,才能决定结果是否有资格进入页面。比较文本本身可以作为第二道防线。

隐藏在现象后的机制

实施选择是:每次更改生成递增 token,仅接受与当前文本和 token 都匹配的响应,再区分检查中、有效和网络未知。这里不能倒过来,先把组件和请求写完,再向现有代码里硬塞判断。规则入口只接收必要数据,输出也尽量可序列化。这样在没有设备的环境下仍能对核心逻辑做确定性检查。

把“输入变化”和“token 递增”分成两个阶段,是为了让每个阶段都有可检查的输入与输出。市集摊位名输入时的远端重名提示并不需要复杂框架才能做到这一点,真正需要的是一个稳定的数据契约:前一步产出的值能被下一步清楚识别,失败时也能停在正确的位置。

从“token 递增”走到“发起校验”时,不要直接复用页面当前显示的全部状态。界面可能正在更新,用户也可能继续操作。此处应只传递本阶段约定的数据,并记录这份数据属于哪次动作,减少跨阶段读到不同版本的风险。

“发起校验”阶段很容易被认为已经完成了整个功能,其实它只提供一个前提。只有进入“比较版本”之后,才能知道前一步是否真的满足业务要求。把这个检查写成显式返回值,比依赖某个内部字段在恰当时刻改变更稳健。

还要把四种状态分开:checking、available、taken、unknown。unknown 是网络不可判断,不代表 taken,更不代表 available;只有真正完成成功校验的状态,才可以满足需要远端唯一性的提交条件。

异步竞态的关键设计

把约束写进实现

下面给出可以独立检查的关键实现。代码只覆盖本篇要证明的核心行为;涉及工程权限、服务端协议或构建设置的条件已在上下文列明。它应被放入明确的模块中,而不是把所有业务逻辑堆进 ArkUI build()。

class NameVerifier {
  private version: number = 0
  private latestText: string = ''
  async check(name: string, call: (text: string) => Promise<boolean>): Promise<string> {
    const ticket = ++this.version
    this.latestText = name
    try {
      const available = await call(name)
      if (ticket !== this.version || name !== this.latestText) return 'stale'
      return available ? 'available' : 'taken'
    } catch (_e) {
      return ticket === this.version ? 'unknown' : 'stale'
    }
  }
}

连续输入时可增加防抖以减少服务端压力,但防抖不能替代版本门禁。即便防抖只发出两个请求,网络依然可能逆序返回;即便某个请求被取消,回调也可能处于完成过程。

代码中的关键判断来自业务约束,而不是某个屏幕比例。以“发起校验”为界,输入应先满足所属阶段的前置条件;当判断失败时,返回错误或保留状态比直接抛出无法解释的页面状态更可控。对需要 UI 反馈的部分,随后由组件转换为明确的文案。

如果把上述核心逻辑接到一个 ArkUI 页面,推荐组件仅处理状态投影和交互事件:例如 TextInput.onChange 接收新的字符串,Button.onClick 触发一次显式动作,得到结果后再设置 @State 驱动展示。对于不是输入框的异步竞态功能,则采用相同的单向数据边界而不强行引入 TextInput。

异步竞态的验证示例

输入越界以后会发生什么

最应该提前考虑的风险是:接口错误不能按“名称可用”处理,取消也不意味着旧请求一定不会回调。这句话不是发布时随手附加的注意事项,而是编码时必须坚持的限制。只要异常路径缺乏明确处理,下一次回归就可能把一个本来可恢复的状态变成难以判断的失败。

构造测试时不要等真实网络偶然变慢。用两个可以手动决定 resolve 顺序的 Promise 代替服务端,先完成 B 再完成 A,观察旧结果是否被丢弃。再让 B 失败,确认页面是“暂不能判断”而不是“名称可用”。

跨版本核对。 在数据和配置持续演进的前提下,固定一份输入,记录预期输出;随后修改不相关的界面颜色或布局,再运行同一用例。如果结果变化,说明异步竞态与展示层之间出现了不应存在的隐性耦合。

边界值核对。 不只输入一个“正常例子”,还需要空字符串、用户明确的否定值、最大长度附近以及重复内容等不同形态。对异步竞态来说,边界值是否被恰当表达,往往比用户第一次成功完成流程更能反映实现质量。

状态验收。 在市集摊位名输入时的远端重名提示中,专门制造一个尚未完成的状态:先输入“晨光”,再改成“晨光手作”,第一个慢响应反而覆盖第二次结果,页面把合法名称标成重复。观察模块是否明确阻止下一动作、是否保留用户已经输入的有效内容,以及重新操作后是否会得到可解释的新结果。这个检查的价值在于不把一次成功操作当作稳定性的全部证据。

逆序验收。 把正常操作反过来执行,例如先触发输入变化,再补齐输入变化所需要的条件。若界面没有把错误留在错误位置,或者隐式使用上一轮结果,就说明阶段约束并未完全落实。真正的测试应覆盖操作顺序变化,而非只按产品示例步骤点击。

对下一次改造的启示

下一篇草稿快照会沿用最新输入的版本语义。历史记录可以保存值,但绝不能保存“某个已经过期的远端校验结论”作为永久事实。

本篇可用以下结果作为验收目标:手动令第一请求晚于第二请求返回,页面依然保留新名称的校验结果。检验时先从固定输入入手,再加入中途切换、重复操作和失败恢复。可解释的错误结果也属于正确实现的一部分;没有必要为了让每个步骤都显示绿色而悄悄跳过不满足的前置条件。

对于业务异常,提示文案至少要回答当前状态、可以继续的动作以及不能继续的原因。市集摊位名输入时的远端重名提示里的用户通常不关心内部类名或错误码,但需要知道是等待、重试还是修改输入。把工程错误类别映射为用户可理解的行为建议,比把原始异常堆栈放在对话框里更合适。

交付的验证证据应当与主张相称。纯函数逻辑可以使用固定输入输出来证明;界面效果只能由真实运行或明确标注的示意展示;网络限流、签名、权限等涉及外部系统的结论,需要在对应环境中完成复核。不要把静态检查结果提升为已经完成真实设备测试的结论。

还有一个容易被忽视的协作问题:页面状态、领域规则和最终输出往往由不同代码文件甚至不同开发者维护。要让改动可控,必须为异步竞态明确最小接口,规定哪些数据可以跨模块传递、哪些只是展示层的临时变量;代码评审只要看到两处重复决定同一事实,就需要回到单一来源原则。

调试不应从增加大量打印开始。先把输入变化 → token 递增 → 发起校验 → 比较版本 → 接受或丢弃按顺序列出来,每次只在边界处记录不含个人信息的阶段与结果类别。这样即使没有真机性能指标,仍能通过构造输入与检查返回值定位哪一步发生分歧;反过来,如果只看最终页面颜色,就难以证明中间没有错误。

关于接口错误不能按“名称可用”处理,取消也不意味着旧请求一定不会回调这一风险,建议把它写进实际测试用例的名称和断言,而不是只放在代码注释。注释会随着重构失效,测试中的输入与预期结果更容易暴露行为变化。测试本身同样要避免依赖随时变化的网络与时间,否则会把产品故障和测试环境偶然因素混在一起。

这类问题的适用边界同样清楚:远端响应存在不确定顺序,必须以输入版本而非请求完成时刻决定结果归属。当具体 SDK、设备能力或服务端协议发生变化,应重新查看对应的官方接口约束,再重新跑上述场景验证,而不是根据一张示例图推断系统行为。

官方资料

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐