React 中 `setState` 的同步与异步行为再总结

1,192 阅读3分钟

在 React 17 及之前版本中,setState 是否同步更新,取决于它被调用的上下文环境。

一、同步更新的情况

1. 在原生事件或 setTimeout/setInterval、Promise 等“异步”环境下

结论:
在这些环境下,setState同步的,也就是说,状态会立即更新,后续代码能拿到最新的 state。

示例1:setTimeout 中 setState

import React, { useState, useEffect } from 'react';

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

  useEffect(() => {
    setTimeout(() => {
      setCount(count + 1);
      console.log('setTimeout后,count:', count); // 这里count是旧值
    }, 1000);
  }, [count]);
 console.log('render了一次,count:', count);
  return <div>{count}</div>;
}

注意:
虽然 setState 在 setTimeout 中是同步的,但由于闭包原因,console.log 里拿到的 count 还是旧值。
如果你在 setTimeout 里连续多次 setState,React 17 及之前会同步执行多次,每次都会触发一次 render。

示例2:原生事件中 setState

import React, { useState, useRef, useEffect } from 'react';

function NativeEventSyncExample() {
  const [count, setCount] = useState(0);
  const btnRef = useRef(null);

  useEffect(() => {
    const handler = () => {
      setCount(count + 1);
      console.log('原生事件后,count:', count); // 这里count是旧值
    };
    btnRef.current.addEventListener('click', handler);
    return () => btnRef.current.removeEventListener('click', handler);
  }, [count]);

  return <button ref={btnRef}>点击</button>;
}

说明:
在原生事件(非 React 合成事件)中,setState 也是同步的。


二、异步批量更新的情况

1. React 合成事件、生命周期函数、useEffect 等

结论:
在 React 的合成事件(如 onClick)、生命周期函数(如 componentDidMount)、useEffect 等环境下,setState异步批量更新的。
也就是说,setState 不会立刻更新 state,只有等事件处理函数执行完毕后,React 才会统一批量更新 state 并重新渲染组件。

示例3:合成事件中 setState

import React, { useState } from 'react';

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

  const handleClick = () => {
    setCount(count + 1);
    console.log('合成事件后,count:', count); // 这里count是旧值
    setCount(count + 1);
    console.log('合成事件后,count:', count); // 这里count还是旧值
  };

  return <button onClick={handleClick}>点击</button>;
}

说明:
在合成事件中,setState 是异步的,console.log 拿到的 count 还是旧值。
而且多次 setState 会被合并(批量更新),只会触发一次 render。


三、详细代码讲解

1. setState 在不同环境下的表现

import React, { useState, useEffect, useRef } from 'react';

function SetStateDemo() {
  const [count, setCount] = useState(0);
  const btnRef = useRef(null);

  // React 合成事件
  const handleClick = () => {
    setCount(count + 1);
    console.log('合成事件中,count:', count); // 旧值
    setCount(count + 1);
    console.log('合成事件中,count:', count); // 旧值
  };

  // 原生事件
  useEffect(() => {
    const handler = () => {
      setCount(count + 1);
      console.log('原生事件中,count:', count); // 旧值
      setCount(count + 1);
      console.log('原生事件中,count:', count); // 旧值
    };
    btnRef.current.addEventListener('click', handler);
    return () => btnRef.current.removeEventListener('click', handler);
  }, [count]);

  // setTimeout
  useEffect(() => {
    setTimeout(() => {
      setCount(count + 1);
      console.log('setTimeout中,count:', count); // 旧值
      setCount(count + 1);
      console.log('setTimeout中,count:', count); // 旧值
    }, 2000);
  }, [count]);

  // Promise
  useEffect(() => {
    Promise.resolve().then(() => {
      setCount(count + 1);
      console.log('Promise中,count:', count); // 旧值
      setCount(count + 1);
      console.log('Promise中,count:', count); // 旧值
    });
  }, [count]);

  return (
    <div>
      <button onClick={handleClick}>React合成事件</button>
      <button ref={btnRef}>原生事件</button>
      <div>Count: {count}</div>
    </div>
  );
}

四、总结

  • React 17 及之前版本,setState 是否同步,取决于调用环境:
    • React 合成事件、生命周期、useEffect 等:异步批量更新
    • 原生事件、setTimeout、Promise 等:同步更新
  • 多次 setState 行为:
    • 合成事件中多次 setState 会被合并,只触发一次 render。
    • 异步环境下多次 setState 不会合并,每次都会触发 render。

五、深入理解

为什么会有这种区别?

React 为了性能优化,在合成事件和生命周期中采用了批量更新机制(Batching),这样可以减少不必要的渲染。而在原生事件、setTimeout、Promise 等环境下,React 无法感知这些事件的边界,所以只能同步更新。


六、扩展

  • React 18 开始,setState 在所有环境下都采用了批量更新(即使在 setTimeout、Promise 里也是异步的),这是 React 18 的新特性。

  • 你可以通过 ReactDOM.unstable_batchedUpdates 手动实现批量更新。