React uses setState to update the state of a class component, but do you really know how it works?

The phenomenon of description

Here is a basic class component:

import React from 'react'

export class SetStateDemo extends React.Component {
  constructor(props) {
    super(props)
    this.state = { number: 0}}render() {
    return (
      <div>
        <p>{this.state.number}</p>
        <button onClick={this.handleClick}>+</button>
      </div>
    )
  }

  handleClick = () = > {
    this.setState({ number: this.state.number + 1}}})Copy the code

When the button is clicked, the internal state number is incremented by 1. This is fine, but when we add two lines of setState to the handleClick function, an interesting question arises:

  handleClick = () = > {
    this.setState({ number: this.state.number + 1 })
    this.setState({ number: this.state.number + 1})}Copy the code

Instead of incrementing number by 2 after each click, the program still increments number by 1. Why? I’m not going to explain why, but to compare another scenario here, after adding the timer wrap:

  handleClick = () = > {
    setTimeout(() = > {
      this.setState({ number: this.state.number + 1 })
      this.setState({ number: this.state.number + 1})})}Copy the code

The value of “number” increases by 2 each time. If this too puzzles you, read on:

The principle of analysis

In React, state updates are asynchronous and batch by default, not synchronous. In other words, when a developer calls setState, it is not executed immediately. Instead, setState is cached until the event function is processed and batch updates are performed only once.

Why would you do that? Consider the following scenario:

  handleClick = () = > {
    this.setState({ number: 1 })
    this.setState({ number: 2 })
    this.setState({ number: 3 })
    this.setState({ number: 4 })
    this.setState({ number: 5})}Copy the code

Frequent calls to setState in event handlers, if updated and rendered immediately each time, can result in frequent page refreshes, which can have a performance impact. In the example above, the final state is number equal to 5, and the middle state is completely unnecessary.

In React, event handlers are merged and updated in batches whenever they are controlled by React. Asynchronous operations such as setTimeout are not merged or updated in batches if they are not controlled by the React main thread. Take a look at the chart below:

React is an important and difficult implementation of React. Make sure you understand how it works.

Non-batch update

We know that class components always inherit from react.component. here’s the implementation logic for that Component:

import { createDOM } from './react-dom'

class Component {
  static isReactComponent = true
  constructor(props) {
    this.props = props
    this.state = {}
  }
  // Non-batch update setState implementation
  setState(nextState, callback) {
    if (typeof nextState === 'function') nextState = nextState(this.state)
    this.state = { ... this.state, ... nextState }this.forceUpdate()
    if (typeof callback === 'function') callback()
  }
​
  // Force refresh
  forceUpdate() {
    this.componentWillUpdate?.() // Call the componentWillUpdate hook
    const oldDOM = this.dom // The actual DOM that the class component currently corresponds to
    const newDOM = createDOM(this.render()) / / the new DOM
    oldDOM.parentNode.replaceChild(newDOM, oldDOM) / / replace
    this.dom = newDOM
    this.componentDidUpdate?.() // Calls the componentDidUpdate hook
  }
​
  render() {
    throw new Error('This method is abstract and requires subclass implementation')}}Copy the code

As you can see, the code is very simple: internally use this.state to save the state, and use setState to update the value of this.state. Then use the createDOM method in the React-DOM library to generate the real DOM from the virtual DOM returned by the render function and replace the old one.

Batch update

That way, setState will be rerendered and replaced every time a developer calls it. To optimize the process, we introduced the updateQueue global object and the Updater class specifically for batch updates:

export const updateQueue = {
  isBatchingUpdate: false.// Whether you are in batch update mode
  updaters: new Set(),
  batchUpdate() {
    for (let updater of updateQueue.updaters) {
      updater.updateClassComponent()
    }
    updateQueue.isBatchingUpdate = false}},// A class dedicated to updating
class Updater {
  constructor(it) {
    this.classInstance = it // An instance of the class component
    this.pendingStates = [] // The state waiting to take effect can be an object or a function
    this.callbacks = [] // Callback array
  }

  addState(partialState, callback) {
    this.pendingStates.push(partialState) // Cache the new state set by the user (possibly functions or objects)
    if (typeof callback === 'function') this.callbacks.push(callback)
    this.emitUpdate() // Notification update
  }

  // It updates whenever the property or state changes
  emitUpdate() {
    if (updateQueue.isBatchingUpdate) {
      updateQueue.updaters.add(this) // If you are in batch update mode, the class component is not updated immediately
    } else {
      this.updateClassComponent() // Update the function component}}// Update the function component immediately
  updateClassComponent() {
    const { classInstance, pendingStates, callbacks } = this
    if (pendingStates.length > 0) {
      const nextState = this.getState() // Compute the new state
      classInstance.state = nextState

      /* Lifecycle hooks refuse to update the scene */
      constflag = classInstance.shouldComponentUpdate? .(classInstance.props, nextState)if (flag === false) return

      classInstance.forceUpdate() // Force an update
      callbacks.forEach(cb= > cb())
      callbacks.length = 0}}// Get the last status of the batch update
  getState() {
    let { state } = this.classInstance
    this.pendingStates.forEach(nextState= > {
      if (typeof nextState === 'function') nextState = nextState(state) state = { ... state, ... nextState } })this.pendingStates.length = 0
    return state
  }
}
Copy the code

It’s not a lot of code, but it does one thing: it provides an interface that allows the developer to cache the setState each time and update it in batches at the end.

Synthetic events

Sync code increments each value by 1:

handleClick = () = > {
  updateQueue.isBatchingUpdate = true
  this.setState({ number: this.state.number + 1 })
  this.setState({ number: this.state.number + 1 })
  updateQueue.batchUpdate()
}
Copy the code

The effect of using asynchronous code is incrementing each value by 2:

handleClick = () = > {
  updateQueue.isBatchingUpdate = true
  setTimeout(() = > {
    this.setState({ number: this.state.number + 1 })
    this.setState({ number: this.state.number + 1 })
  })
  updateQueue.batchUpdate()
}
Copy the code

React believes that the batch update behavior should be built in, so it encapsulates each event listener function. In other words, DOM events starting with on, such as onClick, are no longer native DOM events. React encapsulates a layer of events. This has the following advantages:

  • Batch update logic can be built into all event handlers
  • You can mask the differences between browser implementations of DOM events