同步异步 & 批处理

TIP

属于 React 控制之内的会走异步批处理,控制之外的会走同步非批处理。

  • 在原生事件或异步代码中,setState 是同步,非批处理。
  • 在合成事件或钩子函数中,setState 是异步,批处理。

注 1:对于 setState,类组件中无论前后值是否一致,都会导致重渲染;函数组件中若前后值一致,理论上不会导致重渲染(但仍可能重渲染,不过不会重渲染子组件)。

注 2:React 本身执行的过程和代码都是同步的,只是合成事件和生命周期钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,就是我们所说的异步了。

WARNING

React 18 引入了新特性 Automatic Batching 自动批处理,该特性使得在原生事件或异步代码中也会自动走批处理。后续待研究。

同步 & 非批处理

原生事件

class App extends Component {
  state = { 
    count: 0 
  }

  handleClick = () => {
    this.setState({ count: this.state.count + 1 })
    this.setState({ count: this.state.count + 1 }) 
    this.setState({ count: this.state.count + 1 })  

    console.log(this.state.count) // 3
  }

  componentDidMount() {
    document.body.addEventListener('click', this.handleClick)
  }

  render() {
    return <div>{this.state.count}</div> // 3
  }
}

异步代码

class App extends Component {
  state = { 
    count: 0 
  }

  handleClick = () => {
    setTimeout(() => {
      this.setState({ count: this.state.count + 1 })
      this.setState({ count: this.state.count + 1 }) 
      this.setState({ count: this.state.count + 1 })  
      
      console.log(this.state.count) // 3
    }, 0)
  }

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

合成事件绑定的函数中嵌套异步代码,异步代码中的 setState 采用同步非批处理,遵循就近原则。

异步 & 批处理

合成事件

class App extends Component {
  state = { 
    count: 0 
  }

  handleClick = () => {
    this.setState({ count: this.state.count + 1 })
    this.setState({ count: this.state.count + 1 }) 
    this.setState({ count: this.state.count + 1 })  
    
    console.log(this.state.count) // 0
  }

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

钩子函数

class App extends Component {
  state = { 
    count: 0 
  }

  componentDidMount() {
    this.setState({ count: this.state.count + 1 })
    this.setState({ count: this.state.count + 1 }) 
    this.setState({ count: this.state.count + 1 })  
    
    console.log(this.state.count) // 0
  }

  render() {
    return <div>{this.state.count}</div> // 1
  }
}

TIP

在合成事件和钩子函数中,setState 是异步,批处理。
但有个例外,setState 参数是函数形式时,虽其仍是异步,但不参与批处理。

class App extends Component {
  state = { 
    count: 0 
  }

  handleClick = () => {
    this.setState({ count: this.state.count + 1 })
    this.setState({ count: this.state.count + 1 }) 
    this.setState({ count: this.state.count + 1 })

    this.setState(state => {
      return { count: state.count + 1 }
    })  
    
    console.log(this.state.count) // 0
  }

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

思考题

class Example extends React.Component {
  constructor() {
    super()
    this.state = {
      val: 0
    };
  }

  componentDidMount() {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val); // 第 1 次 log

    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val); // 第 2 次 log

    setTimeout(() => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val); // 第 3 次 log

      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val); // 第 4 次 log
    }, 0);
  }

  render() {
    return null
  }
}

// React 18 之前输出
0、0、2、3

// React 18 之后输出。待研究。
0、0、1、1

参考

Last Updated:
Contributors: Vsnoy