Skip to content

只接受仍然有效的结果 ​

用户快速输入时,后发请求可能先完成,先发请求也可能晚到。正确做法是取消旧请求并保留一个有效性标记;只检查“请求成功”不能证明它仍属于当前界面状态。

ts
let version = 0;
let controller: AbortController | undefined;

async function load(query: string) {
  controller?.abort();
  controller = new AbortController();
  const current = ++version;
  const response = await fetch(`/api?q=${encodeURIComponent(query)}`, { signal: controller.signal });
  const data = await response.json();
  if (current === version) show(data);
}

乐观更新还需要保存回滚信息:界面先显示预期结果,服务端拒绝时恢复旧值并解释失败原因。写操作必须有幂等键或服务端去重,不能把重复点击当成罕见情况。

练习 ​

为“收藏”按钮设计乐观更新:点击后立即改变图标,服务器失败时恢复原状态;连续点击和请求超时不能造成 UI 与服务端永久分叉。

核心机制与实现 ​

状态设计先区分本地 UI、跨组件共享、服务器缓存和 URL 状态;全局 store 不是默认答案。

围绕“只接受仍然有效的结果”先写出输入 → 中间状态 → 可观察结果,再实现最小闭环。不要把事件监听、异步请求、DOM 读取或 store 写入混在一个函数里;每个副作用都要有创建、完成、取消和清理路径。

js
type State = { filter: string; selectedId: string | null }
const initial: State = { filter: '', selectedId: null }
function reducer(state: State, action: Action): State {
  if (action.type === 'filter/changed') return { ...state, filter: action.value }
  return state
}

逐步运行:先只验证成功路径,再改变一个输入;记录时间顺序、对象身份、错误类型和最终可见状态。若环境不支持某个浏览器 API,要把静态协议验证和真实浏览器验证分开记录。

失败边界与变体 ​

  • 输入为空、重复、过期或非法时,不应静默覆盖有效状态。
  • 组件卸载、路由离开、标签页关闭或请求取消时,资源必须停止写入。
  • 跨文档、Shadow DOM、Worker、SSR 或多标签页会改变可见边界,不能沿用单页面直觉。
  • 需要持久化时记录 schema 版本和迁移失败策略;需要全局状态时先证明局部状态不足。

可运行实验与练习 ​

画出状态所有者、读者和写入者;测试刷新、回退、并发请求、错误恢复和持久化迁移,解释为何某个状态不应放进全局 store。

练习要求:实现一个最小版本,补一个失败测试,再给出一个替代方案及其适用条件。答案应包括实际输入序列、观察结果、修复原因和未覆盖的浏览器差异。

查看本节答案

用心记录每一步成长