发布于 更新于 约 1 分钟读完 · 532 字

本文目录 8 节
  1. 问题
  2. 方案:乐观更新 + 失败回退
  3. 核心逻辑
  4. 关键设计决策
  5. 配合服务端持久化
  6. 边界
  7. 来源
  8. 关联文档

前端投票场景的乐观更新实践

适用场景:需要即时反馈的投票/点赞/收藏等交互,API 调用有延迟,用户期望点击后立即看到状态变化。

问题

投票页点红心 → 调 API → 等返回 → 更新 UI。API 延迟 300-500ms 时,用户双击红心会感觉「没反应」,可能重复点击。

方案:乐观更新 + 失败回退

核心逻辑

async function selectVote(submissionId) {
  const previousVote = state.vote;       // ① 保存旧值
  state.vote = submissionId;             // ② 乐观更新
  renderVoteWorks();                     // ③ 立即渲染
  try {
    const result = await api.castVote(submissionId);
    state.vote = result.battle.myVoteSubmissionId; // ④ 以后端为准
  } catch (error) {
    state.vote = previousVote;           // ⑤ 失败回退
    renderVoteWorks();
    showToast(error.message);
  }
}

关键设计决策

  1. 保存旧值而非硬编码 null:如果用户之前已投过作品 A,现在改投作品 B 但失败,应该恢复到 A 而不是清空。previousVote 保存的是”点击之前的状态”。

  2. 以后端返回值为准:即使本地乐观更新成功,服务端返回的 myVoteSubmissionId 可能因竞态条件与本地不同(比如另一个标签页同时改了投票)。第④步用后端值覆盖本地值,保证最终一致性。

  3. 失败回退不是「静默失败」:catch 里同时调了 renderVoteWorks() 回退 UI 和 showToast() 告知用户。只回退不提示,用户不知道操作失败了;只提示不回退,UI 和实际状态不一致。

配合服务端持久化

乐观更新只能解决「点击 → 看到反馈」的延迟,不能解决「刷新 → 状态丢失」。需要服务端同时支持:

  • buildBattle() 返回当前用户的 myVoteSubmissionId
  • 前端 initializeBattle() 拿到后恢复 state.vote

这样刷新后红心仍然亮着,乐观更新的体验闭环才算完整。

边界

  • 乐观更新适合「低风险、高频、期望即时反馈」的交互(投票、点赞、收藏)。不适合「高风险、低频、不可逆」(删除、支付、发布)。
  • 失败回退会有短暂的 UI 闪烁(乐观更新 → 失败 → 回退),这是可接受的折中。
  • 极端情况下(网络极慢,用户在失败回退前已经离开页面),UI 残留的乐观状态不会有实际影响——刷新后会从服务端恢复真实状态。

来源

PB Arena BUG-03 投票阶段乐观更新实践,commit 1b0a90e,2026-08-11。


关联文档

类型/平台工程