小生常谈,react基础useState

36 阅读3分钟

useState详解,摸鱼仔无事,重新再看看常用react hooks

u=1040978320,221962049&fm=253&fmt=auto&app=120&f=JPEG.webp

1. useState 基础用法

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

setCount(count + 1);

关键点补充解释:

  • useState 在组件首次渲染时初始化

  • 后续渲染不会再次执行初始化函数

  • setCount 并不是“直接改值” ,而是:

    向 React 提交一次“更新请求”

这一点非常重要,后面所有“异步 / 批处理 / 丢更新”问题都源自这里。


2️. useState 为什么“异步”?

❗一个常见误区

❌ setState 是 JS 异步
✅ setState 是 React 延迟执行

真正的原因只有一句话:

React 会先收集更新,再统一渲染,而不是来一个更新就渲染一次


一个更贴切的类比

setState 想成:

React.enqueueUpdate(update)

而不是:

state = newState

你调用的是 “登记更新” ,不是“立刻修改”。


3️. 批处理(Batching)到底在干什么?

React 18 之前 vs 之后

React 17 及以前

仅在 React 事件中 批处理:

onClick={() => {
  setA(1)
  setB(2)
}} //  批处理
setTimeout(() => {
  setA(1)
  setB(2)
}) //  不批处理(渲染两次)

React 18(Automatic Batching)

任何地方都会批处理

setTimeout(() => {
  setA(1)
  setB(2)
}) //  只渲染一次
Promise.resolve().then(() => {
  setA(1)
  setB(2)
}) // 渲染一次

React 18 引入了 Automatic Batching


🔬 Demo:观察渲染次数

import { Button } from "antd";
import { useState } from "react";

export default function FuncUpdateDemo() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);

  console.log("render");
  const handleClick = () => {
    setA((prevA) => prevA + 1);
    setA((prevA) => prevA + 1);
    setB((prevB) => prevB + 1);
  };

  return (
    <div className="m-[10px]">
      <div>
        <p>a: {a}</p>
        <p>b: {b}</p>
      </div>

      <div>
        <Button type="primary" onClick={handleClick}>
          a + 1
        </Button>
      </div>
    </div>
  );
}

点击一次:

render

只打印一次,a: 2 ,b:1

image.png


4️. setState 到底什么时候“同步”?什么时候“异步”?

核心判断标准

React 是否能控制这段代码的执行上下文


场景是否批处理原因
React 事件React 包了一层
Promise / async✅(React 18)自动批处理
setTimeout✅(React 18)自动批处理
原生 DOM 事件React 管不到
flushSync人为打断批处理

Demo:原生事件同步更新

useEffect(() => {
  const btn = document.getElementById('btn');

  btn!.addEventListener('click', () => {
    setCount(c => c + 1);
    console.log(count); // 最新值
  });
}, []);

因为 不在 React 管理范围内


用ref拿最新值,不触发render

  const aRef = useRef(0);
  const handleClick = () => {
    aRef.current++;
    console.log(aRef.current);
  };


5️. useState 更新流程

一次 setState 发生了什么?

setState
  ↓
创建 Update 对象
  ↓
放入 Fiber.updateQueue
  ↓
标记 Fiber 为 dirty
  ↓
Scheduler 调度(优先级)
  ↓
Render Phase(计算 JSX)
  ↓
Commit Phase(更新 DOM)

为什么 React 要“等一等”再渲染?

因为 React 支持:

  • 优先级(用户输入 > 网络返回)
  • 中断渲染
  • 并发模式(Concurrent Rendering)
  • 避免频繁渲染,性能优化

6️. 常见陷阱


① 连续 setState 丢更新

const [count, setCount] = useState(0);
setCount(count + 1);
setCount(count + 1);
//count 最终只+1 count=1而非2

为什么?

  • 两次都读的是 同一次 render 的 count
  • React 只是合并了 update,不是重新取值

正确写法

const [count, setCount] = useState(0);
setCount(c => c + 1);
setCount(c => c + 1); //count=2

② 闭包陷阱

useEffect(() => {
  setInterval(() => {
    console.log(count);
  }, 1000);
}, []);

❌ 永远是初始值


正确方式 1:函数式更新

setCount(c => {
  console.log(c);
  return c + 1;
});

正确方式 2:useRef 保存最新值

const countRef = useRef(count);
countRef.current = count;

正确方式 3:依赖值变化

useEffect(() => {
  setInterval(() => {
    console.log(count);
  }, 1000);
}, [count]);

③ useState 不会合并对象(和 class 不同)

const [state, setState] = useState({ a: 1, b: 2 });

setState({ a: 2 }); // b 丢了

正确:

setState(prev => ({ ...prev, a: 2 }));

7️. 深入:为什么批处理是 React 的“命根子”?

假设没有批处理:

setA(1);
setB(2);
setC(3);

3 次 render + 3 次 diff + 3 次 commit

页面会抖,CPU 会炸。


批处理的意义一句话总结:

牺牲“立刻可见”,换取“整体性能”


8️. 最终总结

  • useState 不是 JS 异步,而是 React 延迟调度(react scheduler)
  • 批处理是 React 性能的基石
  • React 18 默认开启 Automatic Batching
  • 连续更新一定用函数式 setState (setState((prev)=>prev+1))
  • 闭包问题本质是“渲染快照”,(拿到的是旧state ‘旧照片’)

🔚彩蛋:一句面试必杀回答

setState 为什么是异步的?

因为 React 要收集多次更新,统一调度渲染,从而减少重复 render,并支持并发和优先级控制。