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

useEffect 与依赖

useEffect 与依赖 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

Effect 连上外部系统之后,还得回答「什么时候重新连」。读完你应该能写完整的依赖数组让定时订阅随配置重建,并亲手复现、诊断漏依赖造成的闭包旧值问题。

问题场景

一个心跳组件,每 interval 毫秒跳一次,间隔由用户调整:

useEffect(() => {
  const id = setInterval(() => setTicks(t => t + 1), interval);
  return () => clearInterval(id);
}, []); // 故意写空:定时器只会建一次

用户把间隔从 1000ms 调成 200ms,界面上的「每 1000ms 一跳」却纹丝不动——依赖数组是空的,Effect 只在挂载时跑过一次,它记住的还是旧的 interval。

核心解释

Effect 的契约是:依赖数组声明「这个 Effect 的逻辑依赖哪些渲染期数据」。React 拿它判断「外部世界的连接需不需要重建」——数组里每个值变了,先执行上一次的清理,再重跑 Effect:

useEffect(() => {
  const id = setInterval(() => setTicks(t => t + 1), interval);
  return () => clearInterval(id);
}, [interval]); // interval 变 → 清旧定时器 → 建新的

两个配套纪律:

  • 依赖必须写全。只要 Effect 逻辑里读了的渲染期值(state、props、context)都该进数组,这是给 React 的诚实声明。写全之后,linter 能帮你查漏,React 能保证 Effect 读到的永远是最新值。
  • 想「只跑一次」也别撒谎。空数组是「逻辑不依赖任何渲染期值」的声明。真需要只建一次连接,用 ref 存可变值,而不是把依赖藏起来。

漏依赖的代价不止「不刷新」,还有更隐蔽的闭包旧值。Effect 的回调函数在创建时捕获了当时的变量;依赖缺失时它一直活着,捕获的一直是老快照:

useEffect(() => {
  const id = setInterval(() => setLog(l => [...l, count]), 1000);
  return () => clearInterval(id);
}, []); // 漏了 count:回调里的 count 永远是挂载时的 0

外面按钮把 count 点到 5,日志里还是一排 0。函数没变,函数看见的世界就永远不变。

示例拆解

实验区上下两块:

  • 心跳区:三个按钮切换 200 / 500 / 1000ms,interval 作为 props 传入,Effect 依赖 [interval]。切换瞬间旧定时器被清理、新定时器建立,文案里的「每 X ms 一跳」和实际节奏永远一致——可以在控制台感受节奏变化;
  • 旧值区:每秒把 count 写进日志的定时器故意漏了依赖,旁边有按钮可以给 count 加一。点几次再看日志:外部 count 已经涨了,日志里还是一排 0;
  • 修复方向就在下方任务里:把 count 加入依赖后,定时器会每秒重建一次(日志功能本身有更好的写法,用函数式更新),这是「写全依赖」的直观代价与正确性保证;
  • 验证方式:调心跳频率看文案跟随;点三次「+1」再观察日志区是否仍然全是 0。

动手任务

打开本课实验区,完成三个小改动:

  1. 把旧值区 Effect 的依赖补全成 [count],验证日志开始记录最新值,同时意识到定时器每秒在重建。
  2. 用函数式更新重写日志逻辑(setLog(l => [...l, l.length]) 之类),让依赖数组可以安全保持为空——体会「用更新函数消除对快照的依赖」这条出路。
  3. 给心跳区加一个「暂停」开关:暂停时不建定时器,恢复时重建(提示:依赖数组里加布尔值)。

常见错误

  • 把依赖数组当性能开关手动删减:删掉「不相关」的依赖换来旧值 bug;正确出路是减少依赖(函数式更新、拆分 Effect、用 ref),而不是瞒报。
  • 依赖里放对象或函数字面量:[options] 里的 options 每次渲染都是新引用,Effect 每帧重建;传基本类型值或在上游用 useMemo 稳定引用。
  • 清理函数漏写:定时器、订阅只建不清,切几次页面就叠了几层重复连接(下一课专门收拾)。
  • 以为闭包旧值只在定时器里出现:任何「Effect 创建的、存活超过一次渲染的函数」都有这问题——事件回调、订阅回调、Promise 链都算。

检查与复盘

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

  • 能说清依赖数组的语义:逻辑依赖的渲染期值的诚实清单,变化即重建
  • 亲眼确认过漏依赖的旧值现象,并能解释闭包捕获的原因
  • 知道两条消除依赖的出路:函数式更新和 ref,而不是删依赖

参考资料

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

阅读代码(React 18+)。初始 count=0。点击按钮把 count 变成 1 之后,控制台输出了什么?

function Demo() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('effect:', count);
  }, []);

  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Q2故障定位中等

bug 现场:组件要把「未读消息 N 条」显示在 document.title 上,N 来自 props.unread。实际标题永远停留在首次渲染的数字,后续 props 更新毫无反应。原因和最可靠的修法是?

function Inbox({ unread }) {
  useEffect(() => {
    document.title = `未读消息 ${unread} 条`;
  }, []); // 空依赖

  return <p>收件箱</p>;
}

读完这一课了?

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

Command Palette

Search for a command to run...