同样是 setCount,为啥定时器里传函数才管用?

19 阅读3分钟
// ❌ 错误

useEffect(() => {

  const timer = setInterval(() => {

    setCount(count + 1); 

  }, 1000);

  return () => clearInterval(timer);

}, []);

  


// ✅ 正确:使用函数式更新

useEffect(() => {

  const timer = setInterval(() => {

    setCount(prev => prev + 1);

  }, 1000);

  return () => clearInterval(timer);

}, []);

分析一下上面两个方法,差别在于一个给setCount传的是计算值一个传的是函数

接下来分析一下为什么第一个方法有问题

首先,空依赖的useEffect只在组件第一次渲染时跑一次。我们可以将组件每一次渲染,理解成一个「独立的快照」,里面的变量(count)、函数都是全新的;空依赖的 useEffect 由于只在第一次渲染时执行,绑定的定时器回调,永远属于「第一次渲染的快照」,只能拿到这次快照里的 count。

以下是执行步骤:

  • 第一次渲染(快照 1) :React 创建了「count=0」这个变量,然后执行 useEffect(空依赖,仅一次),启动定时器,定时器的回调函数被绑定在快照 1 上,只能访问快照 1 里的「count=0」;

  • 定时器执行,setCount (0+1) 后:React 发现 count 变了,触发第二次渲染(快照 2) ,这次 React 会创建一个全新的「count=1」变量(注意:是新的,不是修改原来的 0),然后根据新的 count 更新 DOM,页面显示 1;

  • 但因为 useEffect 只执行一次,定时器是在第一次渲染时创建的,它的回调函数从诞生开始,就「粘」在了快照 1上,这辈子都跳不出去。相应地,它只能访问「count=0」,此时过了一秒,再次触发定时器,由于count + 1 是 1,与最新的count一样,不会触发dom更新。因此,页面不会继续出现2 3 4 5 6 ,而是一直是1

为什么传入函数就可以了?

因为函数式更新是让 React 帮你拿最新状态,而不是自己从闭包作用域里拿
这个问题问到了 React 状态更新的底层设计逻辑,因为当你传给 setCount 的是一个「函数」,而非「直接的计算值」时,React 会对这种「函数式更新」做特殊处理 —— 主动把最新状态传给这个函数的参数(prev),而非让函数自己去外部作用域找状态。

  • setCount(count + 1):让定时器回调自己去「外部作用域」找 count 的值,再计算后传给 setCount(找得到旧的,找不到新的);

  • setCount(prev => prev + 1):告诉 React「我要更新状态,你把当前最新的 count 传给我,我基于这个最新值计算」(让 React 帮你找,它永远能找到最新的)。

传入函数可以找到最新的count值,因此,在定时器执行时,就可以获取到最新的count值,从而视图不断更新,显示 1 -> 2 -> 3...

结论

只要是定时器 / 延时器里更新状态,且更新需要 “基于上一个状态”(比如 + 1、-1、拼接),直接用函数式更新,把算账的活交给 React,绝对不踩坑!