MST

星途 面试题库

面试题:React State更新机制中的批处理原理及应用场景

请详细阐述React 18之前和之后的State更新批处理机制有何不同,为什么要进行这样的改变?在实际项目中,哪些场景会因为批处理机制的变化而受到影响,如何解决这些影响?
23.7万 热度难度
前端开发React

知识考点

AI 面试

面试题答案

一键面试

React 18 之前和之后的 State 更新批处理机制不同点

  1. React 18 之前
    • 批处理范围有限:仅在 React 合成事件(如 onClick、onChange 等)和生命周期函数内会进行批处理。也就是说,在这些函数内部多次调用 setState,React 会将这些状态更新合并成一次,从而避免不必要的重新渲染。例如:
    class Example extends React.Component {
      constructor(props) {
        super(props);
        this.state = { count: 0 };
      }
      handleClick = () => {
        this.setState({ count: this.state.count + 1 });
        this.setState({ count: this.state.count + 1 });
        // React 会将这两次 setState 合并,最终只重新渲染一次
      };
      render() {
        return <button onClick={this.handleClick}>{this.state.count}</button>;
      }
    }
    
    • 非合成事件中不批处理:在原生 DOM 事件(如通过 addEventListener 直接绑定到 DOM 元素的事件)、setTimeoutsetInterval 或 Promise 的回调函数中调用 setState,React 不会进行批处理,每次调用 setState 都会立即触发重新渲染。例如:
    class Example extends React.Component {
      constructor(props) {
        super(props);
        this.state = { count: 0 };
      }
      componentDidMount() {
        document.addEventListener('click', () => {
          this.setState({ count: this.state.count + 1 });
          this.setState({ count: this.state.count + 1 });
          // 这两次 setState 会触发两次重新渲染
        });
      }
      render() {
        return <div>{this.state.count}</div>;
      }
    }
    
  2. React 18 之后
    • 自动批处理范围扩大:不仅在 React 合成事件和生命周期函数内,在 setTimeoutsetInterval、原生 DOM 事件、Promise 的回调函数等任何内部,只要是 React 应用内部的更新,都会进行批处理。例如:
    import React, { useState } from'react';
    const Example = () => {
      const [count, setCount] = useState(0);
      const handleClick = () => {
        setTimeout(() => {
          setCount(count + 1);
          setCount(count + 1);
          // React 18 中这两次 setCount 会合并成一次更新
        }, 0);
      };
      return <button onClick={handleClick}>{count}</button>;
    };
    
    • 并发模式支持:React 18 引入了并发模式,批处理机制为并发模式提供了更好的支持。它允许 React 在不阻塞主线程的情况下,对状态更新进行优化和调度,提高应用的响应性和性能。

为什么要进行这样的改变

  1. 提高一致性:React 18 之前批处理机制在不同类型事件中的不一致性给开发者带来了困扰。扩大批处理范围可以让开发者无需关心事件类型,编写代码时更具一致性,减少意外的重新渲染,提高性能优化的可预测性。
  2. 适应新的应用场景:随着 React 应用越来越复杂,并发模式的引入需要更好的批处理机制来管理状态更新。批处理机制的改变使得 React 能够更好地调度任务,在不阻塞主线程的情况下处理多个状态更新,提升应用的用户体验。

在实际项目中受影响的场景及解决方法

  1. 场景
    • 依赖中间状态的场景:在 React 18 之前,非合成事件等情况下多次 setState 立即执行,开发者可能依赖这种立即执行特性获取中间状态。例如,在原生 DOM 事件中多次 setState 并在后续代码中依赖更新后的状态:
    class Example extends React.Component {
      constructor(props) {
        super(props);
        this.state = { data: [] };
      }
      componentDidMount() {
        document.addEventListener('click', () => {
          this.setState({ data: [...this.state.data, 'item1'] });
          // 这里依赖立即更新后的 state.data 去做一些操作
          const newData = this.state.data.filter(item => item!== 'item1');
          this.setState({ data: newData });
        });
      }
      render() {
        return <div>{this.state.data.join(', ')}</div>;
      }
    }
    
    在 React 18 批处理机制下,上述代码获取的 this.state.data 并不是更新后的状态,因为批处理延迟了状态更新。
  2. 解决方法
    • 使用回调形式的 setState:在 React 18 之前和之后都可以使用回调形式的 setState 来确保获取到最新状态。例如:
    class Example extends React.Component {
      constructor(props) {
        super(props);
        this.state = { data: [] };
      }
      componentDidMount() {
        document.addEventListener('click', () => {
          this.setState((prevState) => ({ data: [...prevState.data, 'item1'] }), () => {
            const newData = this.state.data.filter(item => item!== 'item1');
            this.setState({ data: newData });
          });
        });
      }
      render() {
        return <div>{this.state.data.join(', ')}</div>;
      }
    }
    
    • 使用 useReduceruseReducer 可以更好地管理复杂状态更新逻辑,它返回的 dispatch 函数在 React 18 批处理机制下也能按预期工作。例如:
    import React, { useReducer } from'react';
    const initialState = { data: [] };
    const reducer = (state, action) => {
      switch (action.type) {
        case 'addItem':
          return { data: [...state.data, 'item1'] };
        case'removeItem':
          return { data: state.data.filter(item => item!== 'item1') };
        default:
          return state;
      }
    };
    const Example = () => {
      const [state, dispatch] = useReducer(reducer, initialState);
      const handleClick = () => {
        dispatch({ type: 'addItem' });
        dispatch({ type:'removeItem' });
      };
      return (
        <div>
          <button onClick={handleClick}>操作</button>
          {state.data.join(', ')}
        </div>
      );
    };
    
    这样可以避免依赖中间状态带来的问题,同时利用 React 18 的批处理机制优化性能。