说说 React 中的 setState 执行机制

一、是什么

setState 是 React 中更新组件状态并触发重新渲染的核心机制。在类组件中通过 this.setState() 调用,在函数组件中通过 useState 返回的 setter 函数调用。

理解 setState 的执行机制,特别是它的同步/异步行为和批处理策略,是深入理解 React 更新流程的关键。

二、类组件中的 setState

2.1 基本用法

class Counter extends React.Component {
  state = { count: 0, name: "React" };

  handleClick = () => {
    // 用法一:传入对象(与当前 state 浅合并)
    this.setState({ count: this.state.count + 1 });

    // 用法二:传入函数(接收最新 state,返回更新对象)
    this.setState((prevState) => ({ count: prevState.count + 1 }));

    // 用法三:带回调(在 state 更新且 DOM 更新后执行)
    this.setState({ count: 1 }, () => {
      console.log("更新后的 count:", this.state.count);
    });
  };

  render() {
    return <button onClick={this.handleClick}>{this.state.count}</button>;
  }
}

2.2 状态合并

类组件的 setState 会将传入的对象与当前 state 进行浅合并(Shallow Merge):

class Form extends React.Component {
  state = {
    name: "张三",
    age: 25,
    address: { city: "北京", district: "朝阳" },
  };

  updateAge = () => {
    // 只更新 age,name 和 address 保持不变
    this.setState({ age: 26 });
    // 结果: { name: "张三", age: 26, address: { city: "北京", district: "朝阳" } }
  };

  updateCity = () => {
    // 注意:浅合并只处理第一层
    // 嵌套对象需要手动展开
    this.setState((prev) => ({
      address: { ...prev.address, city: "上海" },
    }));
  };
}

2.3 连续调用的问题

class Counter extends React.Component {
  state = { count: 0 };

  handleClick = () => {
    // 对象形式连续调用——最终 count 只加 1
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
    // 三次调用被合并,都基于同一个 this.state.count (0)
    // 等价于: Object.assign({}, {count: 1}, {count: 1}, {count: 1})
    // 最终 count = 1

    // 函数形式连续调用——最终 count 加 3
    this.setState((prev) => ({ count: prev.count + 1 }));
    this.setState((prev) => ({ count: prev.count + 1 }));
    this.setState((prev) => ({ count: prev.count + 1 }));
    // 每次都基于前一次的结果
    // 最终 count = 3
  };
}

三、React 18 之前的批处理行为

在 React 18 之前,setState 的批处理行为取决于调用时机:

3.1 React 事件处理中——异步批处理

class Example extends React.Component {
  state = { count: 0, flag: false };

  handleClick = () => {
    // 在 React 合成事件中,setState 是"异步"的
    this.setState({ count: 1 });
    console.log(this.state.count); // 0(还没更新)

    this.setState({ flag: true });
    console.log(this.state.flag); // false(还没更新)

    // 两次 setState 会被批处理,只触发一次渲染
  };

  render() {
    console.log("render"); // 只打印一次
    return <button onClick={this.handleClick}>点击</button>;
  }
}

3.2 非 React 上下文中——同步执行(React 18 之前)

class Example extends React.Component {
  state = { count: 0 };

  componentDidMount() {
    // setTimeout 中
    setTimeout(() => {
      this.setState({ count: 1 });
      console.log(this.state.count); // React 17: 1(同步更新了)

      this.setState({ count: 2 });
      console.log(this.state.count); // React 17: 2(同步更新了)
      // React 17: 触发两次渲染
    }, 0);

    // 原生事件中
    document.getElementById("btn").addEventListener("click", () => {
      this.setState({ count: 1 });
      console.log(this.state.count); // React 17: 1(同步更新了)
    });

    // Promise 中
    fetch("/api/data").then(() => {
      this.setState({ count: 1 });
      console.log(this.state.count); // React 17: 1(同步更新了)
    });
  }
}

这种不一致的行为是 React 17 及之前版本的一个痛点,开发者需要判断执行上下文才能预测 setState 的行为。

四、React 18 的自动批处理

React 18 引入了 Automatic Batching(自动批处理),无论在什么上下文中,多次 setState 都会被自动批处理:

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

  useEffect(() => {
    setTimeout(() => {
      setCount((c) => c + 1);
      setFlag((f) => !f);
      // React 17: 两次渲染 | React 18: 一次渲染 ✓
    }, 1000);

    fetch("/api/data").then(() => {
      setCount((c) => c + 1);
      setFlag((f) => !f);
      // React 17: 两次渲染 | React 18: 一次渲染 ✓
    });
  }, []);

  return <div>{count} - {String(flag)}</div>;
}

前提条件:使用 createRoot API 而非旧的 ReactDOM.render

4.1 flushSync 退出批处理

如果确实需要立即执行更新(极少见),可以使用 flushSync

import { flushSync } from "react-dom";

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

  function handleClick() {
    flushSync(() => {
      setCount((c) => c + 1);
    });
    // DOM 已更新,count 已反映到页面

    flushSync(() => {
      setFlag((f) => !f);
    });
    // DOM 再次更新

    // 总共触发两次渲染
  }

  return <button onClick={handleClick}>{count}</button>;
}

flushSync 的使用场景非常有限,主要用于需要在两次状态更新之间读取 DOM 的情况。

五、函数组件中的 useState

5.1 基本机制

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

  function handleClick() {
    setCount(1); // 直接设置值
    setCount((c) => c + 1); // 函数式更新

    // 注意:在同一次渲染中读到的 count 始终是这次渲染的快照
    console.log(count); // 始终是更新前的值
  }

  return <button onClick={handleClick}>{count}</button>;
}

与类组件不同,函数组件中的 state 是闭包捕获的快照,而不是 this.state 引用:

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

  function handleAlertClick() {
    setTimeout(() => {
      // 这里的 count 是点击时刻的值,不是最新值
      alert(`你点击了 ${count} 次`);
    }, 3000);
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount((c) => c + 1)}>+1</button>
      <button onClick={handleAlertClick}>显示</button>
    </div>
  );
}

如果需要获取最新值,使用 useRef

function Timer() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);
  countRef.current = count;

  function handleAlertClick() {
    setTimeout(() => {
      alert(`最新值: ${countRef.current}`);
    }, 3000);
  }

  return (
    <div>
      <button onClick={() => setCount((c) => c + 1)}>+1</button>
      <button onClick={handleAlertClick}>显示最新值</button>
    </div>
  );
}

5.2 状态替换 vs 状态合并

// 函数组件:useState 是替换,不是合并
function Form() {
  const [form, setForm] = useState({ name: "张三", age: 25 });

  function updateAge() {
    setForm({ age: 26 });              // 错误:丢失 name → { age: 26 }
    setForm((prev) => ({ ...prev, age: 26 })); // 正确:{ name: "张三", age: 26 }
  }
}

// 更好的做法:拆分为独立的 state,或使用 useReducer 管理复杂状态

5.3 惰性初始化

当初始 state 的计算比较昂贵时,使用函数形式的初始值:

// 每次渲染都会执行 computeExpensiveValue(浪费)
function Bad() {
  const [data, setData] = useState(computeExpensiveValue());
}

// 只在首次渲染时执行一次(正确)
function Good() {
  const [data, setData] = useState(() => computeExpensiveValue());
}

// 实际例子:从 localStorage 读取
function usePersistedState(key, defaultValue) {
  const [state, setState] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : defaultValue;
  });
  return [state, setState];
}

六、更新优先级

React 18 并发模式下,可以通过 useTransitionuseDeferredValue 标记低优先级更新。高优先级更新(如用户输入)会打断低优先级更新的渲染,保证交互即时响应。startTransition 中的 setState 会被标记为可中断的低优先级更新。

七、总结

维度类组件 setState函数组件 useState
合并策略浅合并(Shallow Merge)替换(Replace)
连续调用对象形式会被合并同为替换
读取值this.state(引用最新)闭包快照(当次渲染的值)
回调setState(obj, callback)无回调,用 useEffect 替代
批处理React 18 前不一致React 18 统一自动批处理
初始化constructor 或 class fields直接传值或惰性函数

面试核心要点:

  1. React 18 实现了全场景自动批处理
  2. 函数式更新 setState(prev => ...) 能保证基于最新值更新
  3. 函数组件的 state 是闭包快照,类组件的 this.state 是引用
  4. flushSync 可以退出批处理(极少使用)
  5. 惰性初始化避免昂贵计算的重复执行