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

副作用与引用综合实验

副作用与引用综合实验 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

这一课没有新概念,是一次综合验收:把请求竞态(R26)、引用管理(R27)、旧闭包(R30)装进同一个页面——可暂停番茄钟加可切换的课程详情,并把「卸载清理」当成一条正式验收线。

问题场景

页面结构:左侧课程标签(3 个,模拟接口延迟不同),右侧番茄钟(可开始 / 暂停 / 重置)。前几课的事故在这里都有条件复发:快速切换课程时旧响应覆盖新页面;番茄钟回调读到旧状态;整页卸载后定时器和请求还在跑。第一版代码的清理台账长这样——空的:

useEffect(() => {
  fetchCourse(courseId).then(setCourse); // 竞态:没有归属检查
}, [courseId]);

useEffect(() => {
  if (!running) return;
  const id = setInterval(tick, 1000); // tick 读旧快照,且没有 clearInterval
}, [running]);

核心解释

综合实验的要点不是写更多代码,而是给每个副作用建立清理台账,动笔前先列出来:

副作用 启动 清理
课程详情请求 fetchCourse(courseId) ignore = true(或 abort)
计时 interval setInterval(tick, 1000) clearInterval(id)
全局监听(如有) addEventListener removeEventListener

验证方法也要提前设计,本课分三条线:

  • 竞态线:点最慢的课程,0.5 秒内点最快的,详情必须停在最后点击的那个。
  • 旧闭包线:番茄钟运行中「暂停」再「开始」,秒数必须连续,不能跳回或加倍走。
  • 卸载线:实验区提供「卸载整个页面」开关;卸载前让请求和计时都在飞行中,卸载后屏幕日志应出现清理记录,且之后再无状态写入。

顺带纠正一个旧观念:React 18 之后「卸载后 setState」的警告已经移除,但这不代表泄漏无害——定时器和回调仍占内存、仍会执行,白耗资源还可能操作已失效的 DOM。

示例拆解

番茄钟的核心:running、seconds 用 state(界面显示),interval id 交给 ref,回调里用函数式更新:

function Pomodoro() {
  const [seconds, setSeconds] = useState(150);
  const [running, setRunning] = useState(false);

  useEffect(() => {
    if (!running) return;
    const id = setInterval(() => {
      setSeconds((s) => (s <= 1 ? 0 : s - 1)); // 不依赖外部快照
    }, 1000);
    return () => clearInterval(id); // 台账第 2 行:暂停、卸载都走这里
  }, [running]);

  return (
    <div>
      <p>{Math.floor(seconds / 60)} 分 {seconds % 60} 秒</p>
      <button onClick={() => setRunning((r) => !r)}>{running ? '暂停' : '开始'}</button>
    </div>
  );
}

课程区沿用 R26 的 ignore 归属策略,并在过期响应到达时往屏幕日志写一条记录:

fetchCourse(courseId).then((data) => {
  if (ignore) { log(`丢弃了课程 ${data.id} 的过期响应`); return; }
  setCourse(data);
  setLoading(false);
});

整页组件再提供一个顶层开关控制挂载 / 卸载;卸载日志由各 Effect 的清理函数写入,这就是「清理真的执行了」的证据。

动手任务

打开本课实验区,按三条验收线逐项操作:

  1. 竞态:点「课程 A(慢)」立刻点「课程 C(快)」,确认详情停在 C,日志出现「丢弃了课程 A 的过期响应」。
  2. 旧闭包:番茄钟走 5 秒,暂停再开始,确认秒数连续不跳变。
  3. 卸载:让慢请求飞行中、计时运行中,点「卸载整个页面」,确认日志出现「清除了计时器」和「丢弃过期响应」,重新挂载后一切从干净状态开始。

常见错误

  • 清理写一半:请求作了废、interval 忘清,泄漏照样发生;对照台账逐行检查。
  • 清理代码和副作用分家:谁发起谁清理,清理必须和启动写在同一个 Effect 里,跨组件代管必漏。
  • 重挂载后状态没复位:要复位的状态放在会被卸载的组件内部,别提到外面全局存。
  • 只用 console 验证:沙箱里更要看屏幕日志,确认清理发生的时间点符合预期。

检查与复盘

完成下方检查题确认理解。自查清单:

  • 三条验收线(竞态、旧闭包、卸载清理)全部按清单走过一遍
  • 能为页面里每个 useEffect 写出「启动 / 清理」对照表
  • 知道卸载后 setState 不再报警告,但清理仍是硬要求

参考资料

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

bug 现场:聊天页每 5 秒轮询新消息,代码如下。快速进出页面几次后,控制台出现「对已卸载组件执行状态更新」类警告,同一条消息还被处理了多次。原因和修法是?

function ChatRoom() {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const id = setInterval(async () => {
      const res = await fetch('/api/messages');
      const data = await res.json();
      setMessages(data);
    }, 5000);
  }, []);

  return <MessageList messages={messages} />;
}

读完这一课了?

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

Command Palette

Search for a command to run...