React 要点 函数式更新 2

0 阅读3分钟

React 要点 函数式更新 2

React 中最容易踩坑的地方之一 :闭包问题和函数式更新

通过具体示例详细解释闭包问题,以及函数式更新如何解决它。。

闭包问题是什么?(具体示例)

假设这个计数器组件:

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

  const handleClick = () => {
    // 问题代码:直接使用闭包中的count
    setTimeout(() => {
      setCount(count + 1);
    }, 3000);
  };

  return (
    <div>
      <p>当前值: {count}</p>
      <button onClick={handleClick}>3秒后+1</button>
    </div>
  );
}

操作步骤:

  1. 初始状态:count = 0
  2. 快速点击按钮 3 次(在 3 秒内连续点击)
  3. 等待 3 秒后查看结果

预期结果:
count 应该变成 3(因为点了 3 次)

实际结果:
count 只变成 1

为什么会这样?

  1. 闭包陷阱

    • 每次点击时,handleClick 函数都捕获了当时count
    • 第一次点击:捕获 count=0 → setCount(0 + 1)
    • 第二次点击:捕获 count=0 → setCount(0 + 1)
    • 第三次点击:捕获 count=0 → setCount(0 + 1)
  2. 状态更新是异步的

    • React 不会立即更新状态
    • 3 次点击都在同一个渲染周期内,使用的都是初始状态值

函数式更新如何解决?

修改为函数式更新:

const handleClick = () => {
  setTimeout(() => {
    // 使用函数式更新
    setCount((prev) => prev + 1);
  }, 3000);
};

现在的结果:

  • 无论点击多少次,3 秒后都会正确累加
  • 点击 3 次 → count=3

为什么函数式更新能解决闭包问题?

  1. 不依赖闭包中的值

    // 直接更新:依赖外部变量 count(闭包陷阱)
    setCount(count + 1);
    
    // 函数式更新:不依赖外部变量
    setCount((prev) => prev + 1);
    
  2. React 保证提供最新值

    • 当 React 执行状态更新时
    • 它会将当前最新状态作为参数传入更新函数
    • 无论何时调用,prev 总是最新的状态值

执行过程对比

问题代码的执行流程:
点击1: setCount(0 + 1)  // 闭包count=0
点击2: setCount(0 + 1)  // 闭包count=0 ❌
点击3: setCount(0 + 1)  // 闭包count=0 ❌
结果: count=1           // 三次都设置成1
函数式更新的执行流程:
点击1: setCount(prev => prev + 1) // 排队更新函数1
点击2: setCount(prev => prev + 1) // 排队更新函数2
点击3: setCount(prev => prev + 1) // 排队更新函数3

React依次执行:
  执行函数1: prev=00+1=1
  执行函数2: prev=11+1=2
  执行函数3: prev=22+1=3
结果: count=3

其他常见闭包问题场景

1. 事件监听器
useEffect(() => {
  const handleScroll = () => {
    // 这里获取的是初始状态
    console.log(count);
  };

  window.addEventListener("scroll", handleScroll);

  return () => window.removeEventListener("scroll", handleScroll);
}, []);
2. setInterval 定时器
useEffect(() => {
  const timer = setInterval(() => {
    // 永远输出初始值
    console.log(count);
  }, 1000);

  return () => clearInterval(timer);
}, []);

解决方案总结

场景错误写法正确写法
状态更新setCount(count + 1)setCount(prev => prev + 1)
事件监听直接使用状态使用 ref 保存最新值
定时器直接使用状态结合 useRef+useEffect

函数式更新的本质:
通过让 React 提供最新状态值,而不是依赖闭包中的过期值,从根本上解决了状态更新的闭包问题。