setState 同步异步问题

React 中 setState 在不同场景下的执行时机和批处理机制

问题

setState 是同步还是异步?在合成事件、原生事件、setTimeout 中表现有何不同?

解答

结论

setState 本身是同步执行的,但 React 会对状态更新进行批处理(Batching),导致看起来像是”异步”的。

  • React 17 及之前:只在合成事件和生命周期中批处理
  • React 18:所有场景都自动批处理

React 17 的行为

import React, { Component } from 'react';

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

  // 合成事件:批处理,"异步"表现
  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count); // 0,还没更新
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count); // 0,还没更新
    // 最终 count 为 1,两次 setState 被合并
  };

  // 原生事件:不批处理,同步更新
  bindNativeEvent = () => {
    document.getElementById('btn').addEventListener('click', () => {
      this.setState({ count: this.state.count + 1 });
      console.log(this.state.count); // 1,立即更新
      this.setState({ count: this.state.count + 1 });
      console.log(this.state.count); // 2,立即更新
    });
  };

  // setTimeout:不批处理,同步更新
  handleTimeout = () => {
    setTimeout(() => {
      this.setState({ count: this.state.count + 1 });
      console.log(this.state.count); // 1,立即更新
      this.setState({ count: this.state.count + 1 });
      console.log(this.state.count); // 2,立即更新
    }, 0);
  };

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

React 18 的自动批处理

import { useState } from 'react';
import { flushSync } from 'react-dom';

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

  // React 18:所有场景都批处理
  const handleClick = () => {
    setTimeout(() => {
      setCount(c => c + 1);
      setCount(c => c + 1);
      // 两次更新被合并,只触发一次渲染
      // 最终 count 为 2
    }, 0);
  };

  // 使用 flushSync 强制同步更新
  const handleFlushSync = () => {
    flushSync(() => {
      setCount(c => c + 1);
    });
    // 此时 DOM 已更新
    console.log(document.getElementById('count').textContent); // 更新后的值

    flushSync(() => {
      setCount(c => c + 1);
    });
    // 触发两次渲染
  };

  return <div id="count">{count}</div>;
}

获取更新后的值

// Class 组件:使用回调
this.setState({ count: 1 }, () => {
  console.log(this.state.count); // 1
});

// 函数组件:使用 useEffect
useEffect(() => {
  console.log(count); // 更新后的值
}, [count]);

批处理原理简述

// 简化的批处理机制
let isBatchingUpdates = false;
let pendingUpdates = [];

function setState(update) {
  pendingUpdates.push(update);
  
  if (!isBatchingUpdates) {
    // 不在批处理中,立即执行
    flushUpdates();
  }
  // 在批处理中,等待统一执行
}

function batchedUpdates(fn) {
  isBatchingUpdates = true;
  fn(); // 执行期间的 setState 都会被收集
  isBatchingUpdates = false;
  flushUpdates(); // 统一处理所有更新
}

关键点

  • setState 本身是同步的,批处理让它表现得像异步
  • React 17:合成事件和生命周期中批处理,原生事件和 setTimeout 中同步
  • React 18:所有场景自动批处理(Automatic Batching)
  • 使用 flushSync 可以强制同步更新,跳出批处理
  • 获取更新后的值:Class 用回调,函数组件用 useEffect