R43第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

写入、乐观更新与回滚

写入、乐观更新与回滚 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

读完你能为「收藏 / 点赞」这类写操作实现完整闭环:立即改界面(乐观)、请求确认、失败回滚;能处理快速连点与并发响应,并用「界面态 vs 服务端态」的对照验证最终一致。

问题场景

课程卡片的收藏星标:点一下,请求 800ms 后成功。这 800ms 里用户得不到任何反馈,于是再点两下——三个请求排队,收藏状态翻转三次,最终结果取决于服务器处理顺序,连用户自己都说不清到底收藏了没有。写操作的两难:等服务器,体验差;不等服务器,就得自己保证「错了能改回来」。

核心解释

乐观更新的标准闭环:乐观写 → 服务端确认 → 失败回滚,加上连点防抖:

async function toggleFav(id) {
  if (pending[id]) return;                       // 连点防抖:该对象处理中,忽略
  const nextValue = !favs[id];                   // 本次意图
  setFavs((f) => ({ ...f, [id]: nextValue }));   // 1. 乐观写:界面立刻变
  setPending((p) => ({ ...p, [id]: true }));
  try {
    const server = await setFavoriteApi(id, nextValue); // 2. 服务端确认
    setFavs((f) => ({ ...f, [id]: server }));    // 3. 以服务器返回校准
  } catch (e) {
    setFavs((f) => ({ ...f, [id]: !nextValue })); // 4. 失败:只回滚本键
    setError("收藏失败,已恢复");
  } finally {
    setPending((p) => ({ ...p, [id]: false }));
  }
}

两个容易出错的细节。第一,回滚要最小化:只恢复本次操作改的那个键,不能把用户期间的其他操作一起回滚——所以用函数式更新,按对象逐键恢复。第二,并发响应的最终一致:多个对象的请求并行时,每个响应只校准自己的键,互不覆盖:

// 并发响应落地:每个响应只校准自己那门课的键
setUiFavs((f) => ({ ...f, [resp.id]: resp.value }));
setServerFavs((s) => ({ ...s, [resp.id]: resp.value }));

若服务器返回整份列表,就要警惕「旧响应后到覆盖新状态」——这是请求竞态在写操作里的变体,解法同样是序号守卫。React 18 也提供了 useOptimistic 把「乐观值 + 自动回滚」封装成原语,底层思路与手写一致。验证方式:把「界面收藏集」和「服务器收藏集」并排显示,连点、开失败模式,观察两侧从分歧到一致的全过程。

示例拆解

实验区 6 门课程可收藏,每门课请求延迟 800ms:

  • 每张卡片显示星标(界面态)与处理中标记;右侧面板显示服务器实际收到的收藏集与两侧一致性;
  • 连点同一门课 3 次:处理中被锁定,只发一个请求;
  • 开「模拟失败」后点收藏:星标先亮,800ms 后弹回,提示「收藏失败,已恢复」,服务器集合不变;
  • 关掉失败模式,连点不同课程:每个响应只校准自己的键,最终两侧一致;
  • 验证:交替执行上述操作,盯住「界面态与服务端态」的对照指示。

动手任务

打开本课实验区:

  1. 把「处理中忽略连点」改成「排队执行」:上一次完成后自动补发最新意图,测试连点 3 次的最终结果。
  2. 给回滚加重试:失败恢复后自动再试一次(最多 1 次),验证最终一致。
  3. 把两门课的延迟改成随机(300-1000ms),快速操作后确认没有出现旧响应覆盖新状态;说明你的校准逻辑为什么安全。

常见错误

  • 乐观写后不校准:以为界面写了就完事,服务器返回的最终值(可能被别人改过)没有落地。
  • 回滚直接清空整份状态:把用户期间的其他操作一起回滚了;回滚只针对本次操作影响的那部分。
  • 连点不设防:同一对象堆 N 个请求,最终状态等于最后一次响应,而不是用户最后一次点击的意图。
  • 用整份服务器响应直接覆盖:乱序响应会覆盖较新的本地状态;按响应与对象的对应关系校准,过期响应丢弃。

检查与复盘

自查清单:

  • 能按「乐观写 → 确认 → 回滚」复述闭环,说出每一步对应哪行代码
  • 连点同一课程只发一个请求(或排队),验证过
  • 失败回滚后界面态与服务器态重新一致,验证过

参考资料

动手实验
动手实验
预览
点击「运行」在隔离沙箱中预览
检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1读代码较难

阅读代码(React 18+):点赞先乐观 +1,请求失败再回滚。本次请求必然失败。点击一次「赞」按钮后,按钮上数字如何变化、最终显示几?这种回滚写法可靠吗?

function LikeButton({ postId }) {
  const [likes, setLikes] = useState(10);

  async function like() {
    setLikes(likes + 1);      // ① 乐观 +1
    try {
      await api.like(postId); // ② 服务端请求(本次必然失败)
    } catch {
      setLikes(likes);        // ③ 失败回滚
    }
  }

  return <button onClick={like}>赞 {likes}</button>;
}
Q2单选简单

下列哪个交互最适合使用乐观更新?

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...