返回首页

说说 React setState 的原理?

问题解析(面试官考察点)

面试官通过此问题主要考察:

  • 对 setState 异步特性的理解
  • 对 setState 批量更新机制的理解
  • 对 React 更新流程的理解
  • 对 state 更新合并策略的理解

核心概念(基础知识点)

setState 是什么

setState 是 React 类组件中用于更新状态的方法。它会触发组件的重新渲染,但更新可能是异步的,并且可能会被批量处理。

setState 的核心特点

  1. 异步更新: setState 不会立即更新 state,而是将更新加入队列
  2. 批量处理: 多个 setState 调用可能会被合并为一次更新
  3. 状态合并: 新的 state 会与旧 state 进行浅合并

详细解答(代码示例)

setState 的基本使用

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      name: 'React'
    };
  }

  // 基本用法:传入对象
  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
  };

  // 传入函数(推荐)
  handleClickBetter = () => {
    this.setState((prevState, props) => ({
      count: prevState.count + 1
    }));
  };

  // 传入回调函数
  handleClickWithCallback = () => {
    this.setState(
      { count: this.state.count + 1 },
      () => {
        console.log('State updated:', this.state.count);
      }
    );
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick}>+1</button>
      </div>
    );
  }
}

setState 的异步特性

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

  handleClick = () => {
    console.log('Before setState:', this.state.count); // 0

    this.setState({ count: this.state.count + 1 });

    console.log('After setState:', this.state.count); // 仍然是 0!
  };

  // 连续多次调用 setState
  handleMultiple = () => {
    // 以下三次调用,count 只增加了 1(React 18 之前)
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });

    // 结果:count = 1,而不是 3
  };

  // 使用函数形式解决
  handleMultipleCorrect = () => {
    // 每次更新都基于最新的 state
    this.setState(prevState => ({ count: prevState.count + 1 }));
    this.setState(prevState => ({ count: prevState.count + 1 }));
    this.setState(prevState => ({ count: prevState.count + 1 }));

    // 结果:count = 3
  };

  render() {
    console.log('render:', this.state.count);
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick}>Test Async</button>
        <button onClick={this.handleMultiple}>Multiple (Wrong)</button>
        <button onClick={this.handleMultipleCorrect}>Multiple (Correct)</button>
      </div>
    );
  }
}

setState 何时是同步的

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

  // React 18 之前:在 setTimeout 中是同步的
  handleTimeout = () => {
    setTimeout(() => {
      console.log('Before:', this.state.count); // 0
      this.setState({ count: this.state.count + 1 });
      console.log('After:', this.state.count); // 1(同步更新)
    }, 0);
  };

  // 原生事件中是同步的
  componentDidMount() {
    document.getElementById('native-btn').addEventListener('click', () => {
      console.log('Before:', this.state.count);
      this.setState({ count: this.state.count + 1 });
      console.log('After:', this.state.count);
    });
  }

  // React 合成事件中是异步的
  handleSynthetic = () => {
    console.log('Before:', this.state.count); // 0
    this.setState({ count: this.state.count + 1 });
    console.log('After:', this.state.count); // 0(异步)
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleSynthetic}>Synthetic Event</button>
        <button onClick={this.handleTimeout}>SetTimeout</button>
        <button id="native-btn">Native Event</button>
      </div>
    );
  }
}

React 18 的自动批处理

// React 18 中所有更新都会自动批处理
class React18Demo extends React.Component {
  state = { count: 0, flag: false };

  handleClick = () => {
    // 在 React 18 中,这些更新会被自动批处理
    setTimeout(() => {
      this.setState({ count: this.state.count + 1 });
      this.setState({ flag: !this.state.flag });
      // 只会触发一次 render
    }, 0);
  };

  // 使用 flushSync 强制同步更新
  handleFlushSync = () => {
    import('react-dom').then(({ flushSync }) => {
      flushSync(() => {
        this.setState({ count: this.state.count + 1 });
      });
      // 这里可以立即获取到更新后的值
      console.log(this.state.count);

      flushSync(() => {
        this.setState({ flag: !this.state.flag });
      });
    });
  };

  render() {
    console.log('render');
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <p>Flag: {this.state.flag.toString()}</p>
        <button onClick={this.handleClick}>Auto Batch</button>
        <button onClick={this.handleFlushSync}>Flush Sync</button>
      </div>
    );
  }
}

深入理解(原理剖析)

setState 执行流程

┌─────────────────────────────────────────────────────────────┐
│                   setState 执行流程                          │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  1. 调用 setState(newState)                                  │
│           │                                                  │
│           ▼                                                  │
│  2. 将更新加入队列(enqueueSetState)                        │
│           │                                                  │
│           ▼                                                  │
│  3. 判断是否处于批量更新模式                                  │
│           │                                                  │
│     ┌─────┴─────┐                                            │
│     ▼           ▼                                            │
│  是(批量)     否(同步)                                        │
│     │           │                                            │
│     ▼           ▼                                            │
│  加入队列     立即执行                                        │
│  延迟执行     batchedUpdates                                 │
│     │                                                        │
│     ▼                                                        │
│  4. 批量更新结束后,统一执行                                  │
│           │                                                  │
│           ▼                                                  │
│  5. 合并 state(Object.assign)                              │
│           │                                                  │
│           ▼                                                  │
│  6. 触发重新渲染(re-render)                                │
│                                                              │
└─────────────────────────────────────────────────────────────┘

setState 源码简化版

// 简化版 setState 实现原理

// 1. setState 入口
Component.prototype.setState = function(partialState, callback) {
  // 将 setState 加入更新队列
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

// 2. 更新队列处理
const updater = {
  enqueueSetState(inst, payload, callback) {
    // 创建更新对象
    const update = {
      payload,
      callback,
      next: null
    };

    // 加入队列
    const queue = inst._internalFiber.updateQueue;
    if (queue === null) {
      inst._internalFiber.updateQueue = update;
    } else {
      // 添加到队列末尾
      let last = queue;
      while (last.next) {
        last = last.next;
      }
      last.next = update;
    }

    // 调度更新
    scheduleUpdateOnFiber(inst._internalFiber);
  }
};

// 3. 批量更新处理
let isBatchingUpdates = false;
let batchedUpdatesQueue = [];

function scheduleUpdateOnFiber(fiber) {
  if (isBatchingUpdates) {
    // 批量更新模式:加入队列,延迟执行
    batchedUpdatesQueue.push(fiber);
  } else {
    // 非批量模式:立即执行
    performSyncWorkOnRoot(fiber);
  }
}

// 4. 批量更新执行
function flushBatchedUpdates() {
  isBatchingUpdates = false;

  // 处理所有队列中的更新
  while (batchedUpdatesQueue.length > 0) {
    const fiber = batchedUpdatesQueue.shift();
    performSyncWorkOnRoot(fiber);
  }
}

批量更新机制

// React 合成事件中的批量更新
class BatchDemo extends React.Component {
  state = { count: 0 };

  handleClick = () => {
    // React 合成事件会自动开启批量更新
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
    // 结果:count = 1(三次更新合并为一次)
  };

  render() {
    console.log('render:', this.state.count);
    return (
      <button onClick={this.handleClick}>
        Count: {this.state.count}
      </button>
    );
  }
}

// 批量更新的实现原理
function batchedUpdates(fn, a, b, c, d, e) {
  const previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingUpdates = true;

  try {
    return fn(a, b, c, d, e);
  } finally {
    // 事件处理结束后,统一执行更新
    isBatchingUpdates = previousIsBatchingUpdates;
    if (!isBatchingUpdates) {
      flushBatchedUpdates();
    }
  }
}

状态合并策略

// setState 使用 Object.assign 进行浅合并

// 初始 state
this.state = {
  count: 0,
  name: 'React',
  user: {
    age: 18
  }
};

// setState 只更新 count
this.setState({ count: 1 });

// 合并后的 state(浅合并)
{
  count: 1,        // 更新
  name: 'React',   // 保留
  user: {          // 保留(引用不变)
    age: 18
  }
}

// 注意:嵌套对象需要手动合并
// 错误的更新方式
this.setState({ user: { age: 19 } });
// 结果:user 对象被完全替换,其他属性丢失

// 正确的更新方式
this.setState(prevState => ({
  user: {
    ...prevState.user,
    age: 19
  }
}));

最佳实践

1. 使用函数式更新

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

  // 推荐:使用函数形式
  increment = () => {
    this.setState(prevState => ({
      count: prevState.count + 1
    }));
  };

  // 连续更新时,函数形式确保正确
  incrementThree = () => {
    this.setState(prevState => ({ count: prevState.count + 1 }));
    this.setState(prevState => ({ count: prevState.count + 1 }));
    this.setState(prevState => ({ count: prevState.count + 1 }));
    // 结果:count 增加 3
  };

  render() {
    return (
      <div>
        <p>{this.state.count}</p>
        <button onClick={this.increment}>+1</button>
        <button onClick={this.incrementThree}>+3</button>
      </div>
    );
  }
}

2. 获取更新后的状态

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

  handleClick = () => {
    // 方式1:使用回调函数
    this.setState(
      { count: this.state.count + 1 },
      () => {
        console.log('Updated:', this.state.count);
        // 可以在这里执行基于新状态的操作
      }
    );

    // 方式2:使用 componentDidUpdate
  };

  componentDidUpdate(prevProps, prevState) {
    if (prevState.count !== this.state.count) {
      console.log('Count changed:', this.state.count);
    }
  }

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

3. 避免直接修改 state

class StateMutation extends React.Component {
  state = {
    items: [1, 2, 3],
    user: { name: 'John', age: 30 }
  };

  // 错误:直接修改 state
  wrongUpdate = () => {
    this.state.items.push(4); // 直接修改!
    this.setState({ items: this.state.items });
  };

  // 正确:创建新对象
  correctUpdate = () => {
    this.setState({
      items: [...this.state.items, 4]
    });
  };

  // 正确:更新嵌套对象
  updateUser = () => {
    this.setState(prevState => ({
      user: {
        ...prevState.user,
        age: 31
      }
    }));
  };

  render() {
    return (
      <div>
        <button onClick={this.correctUpdate}>Add Item</button>
      </div>
    );
  }
}

4. 状态提升与拆分

// 避免一个组件管理过多状态
class BadComponent extends React.Component {
  state = {
    user: {},
    posts: [],
    comments: [],
    ui: { loading: false, error: null }
  };
  // ...
}

// 推荐:拆分状态到不同组件或使用多个 state
function GoodComponent() {
  const [user, setUser] = useState({});
  const [posts, setPosts] = useState([]);
  const [comments, setComments] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // 或者使用自定义 Hook
  const userState = useUser();
  const postsState = usePosts();

  // ...
}

面试要点

  1. setState 是异步的: 不会立即更新 state,而是将更新加入队列

  2. 批量更新: 多个 setState 可能会被合并为一次更新

    • React 合成事件中自动批量更新
    • React 18 中所有更新都自动批处理
  3. 状态合并: 使用 Object.assign 进行浅合并

    • 嵌套对象需要手动展开
  4. 两种调用方式:

    • 对象形式:setState({ count: 1 })
    • 函数形式:setState(prev => ({ count: prev.count + 1 }))
    • 连续更新时推荐使用函数形式
  5. 获取更新后的值:

    • setState 回调函数
    • componentDidUpdate 生命周期
    • 或 await Promise(不直接支持)
  6. React 18 变化:

    • 自动批处理扩展到所有场景
    • 使用 flushSync 强制同步更新