This post has been updated on my Github blog:

preface

This setState (source code involved) is written for the act15 version, i.e. Fiber is not involved; For the convenience of reading and writing, I choose the old version, Fiber is a little difficult to write, so I will write it for now. React 15 uses setState, and React 16 does the same thing.

Although React15 has used React Hooks for a long time, the form of this.setstate () is rarely used, but it is still from the perspective of review and summary to view the changes and development of React. Therefore, it has recently started to review some of the theoretical issues that were only vaguely understood before and slowly accumulate technology.

SetState classic problem

setState(updater, [callback])
Copy the code

React updates state with this.setstate (), and when this.setstate () is used React calls the Render method to re-render the UI.

I don’t need to tell you how to use setState.

Batch update

import React, { Component } from 'react'

class App extends Component {
  state = {
    count: 1,
  }

  handleClick = () = > {
    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) / / 1

    this.setState({
      count: this.state.count + 1,})console.log(this.state.count) / / 1
  }
  render() {
    return (
      <>
        <button onClick={this.handleClick}>Add 1</button>
        <div>{this.state.count}</div>
      </>)}}export default App
Copy the code

Click the button to trigger the event, the print is 1, the page shows the value of count is 2.

This is often referred to as setState batch update. If you perform setState multiple times for the same value, the setState batch update strategy will overwrite it and take the last execution result. So the value printed immediately after each setState is the initial value of 1, and the value displayed on the last page is the result of the last execution, which is 2.

setTimeout

import React, { Component } from 'react'

class App extends Component {
  state = {
    count: 1,
  }

  handleClick = () = > {
    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) / / 1

    setTimeout(() = > {
      this.setState({
        count: this.state.count + 1,})console.log(this.state.count) / / 3})}render() {
    return (
      <>
        <button onClick={this.handleClick}>Add 1</button>
        <div>{this.state.count}</div>
      </>)}}export default App
Copy the code

Click the button to trigger the event. It is found that the print value of count in setTimeout is 3, and the page displays the value of count as 3. In setTimeout, setState is immediately returned to the latest value.

In setTimeout, setState is synchronous; After the previous two setState batch updates, the count value has been updated to 2. The setTimeout first gets the new count value 2, setState again, and then gets the count value 3 in real time.

DOM native event

import React, { Component } from 'react'

class App extends Component {
  state = {
    count: 1,}componentDidMount() {
    document.getElementById('btn').addEventListener('click'.this.handleClick)
  }

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

    this.setState({
      count: this.state.count + 1,})console.log(this.state.count) / / 3

    this.setState({
      count: this.state.count + 1,})console.log(this.state.count) / / 4
  }

  render() {
    return (
      <>
        <button id='btn'>Trigger native event</button>
        <div>{this.state.count}</div>
      </>)}}export default App
Copy the code

Click the button and you will find that the value printed by setState is obtained in real time without batch update.

In DOM native events, setState is also synchronized.

SetState Synchronization asynchrony problem

By synchronous and asynchronous we mean not whether setState is executed asynchronously or what asynchronous code is used, but whether this.state can be updated immediately after setState is called.

React events are synthetic events encapsulated inside React. React executes synchronous procedures and code. However, the sequence of calls to composite events and hook functions precedes the update. As a result, the updated values in composite events and hook functions are not immediately available.

It can also be seen from the above that setState is synchronized in both native events and setTimeout.

SetState source code level

The React version is 15.6.2

SetState function

Inside the source code, setState function code

The React component inherits from React.component.setState is react.component.method

ReactComponent.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState)
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState')}}Copy the code

Can see it directly invoke this. Updater. EnqueueSetState this method.

enqueueSetState

enqueueSetState: function(publicInstance, partialState) {
  // Get the corresponding component instance
  var internalInstance = getInternalInstanceReadyForUpdate(
    publicInstance,
    'setState',);// Queue corresponds to the state array of a component instance
  var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
  queue.push(partialState); // Put partialState into the state to update queue
  // Process the current component instance
  enqueueUpdate(internalInstance);
}
Copy the code

_pendingStateQueue Indicates the queue to be updated

EnqueueSetState does two things:

  • Put the new state in the component’s state queue.
  • EnqueueUpdate is used to process the instance object to be updated.

Let’s see what enqueueUpdate does:

function enqueueUpdate(component) {
  ensureInjected()
  // isBatchingUpdates identifies whether the batch update process is currently in progress
  if(! batchingStrategy.isBatchingUpdates) {// Update components immediately if they are not currently in the batch creation/update phase
    batchingStrategy.batchedUpdates(enqueueUpdate, component)
    return
  }
  // For batch updates, place components in the dirtyComponents queue first
  dirtyComponents.push(component)
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1}}Copy the code

BatchingStrategy indicates the batch update policy. IsBatchingUpdates indicates whether the batch update process is in progress. The default value is false.

EnqueueUpdate does:

  • Check whether the components are in batch update mode. If yes, that is, isBatchingUpdates are true, the state is not updated, but the components to be updated are added to the dirtyComponents array.

  • If you are not in batch update mode, the batchedUpdates method is executed on all the updates in the queue

The batchingStrategy isBatchingUpdates property of the object directly determines whether the update process should be started immediately or whether it should be queued. Therefore, we can probably know that batchingStrategy is used to control the objects of batch update.

Take a look at the source code:

/** * batchingStrategy source code **/
var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false.// An initial value of false indicates that no batch updates are being performed

  // The method to initiate the update action
  batchedUpdates: function (callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates

    ReactDefaultBatchingStrategy.isBatchingUpdates = true

    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e)
    } else {
      // Start the transaction and execute the callback in the transaction
      return transaction.perform(callback, null, a, b, c, d, e)
    }
  },
}
Copy the code

When React calls batchedUpdate to perform an update, the first change is calledisBatchingUpdatesIf this parameter is set to true, the batch update process is in progress.

Transaction. Perform; React Transaction; perform

Transaction mechanism

Transaction is the creation of a black box that encapsulates any method. Therefore, methods that need to be run before and after a function run can be wrapped in this method (these fixed methods can run even if an exception is thrown during a function run).

React: Transaction: Transaction: Transaction: Transaction: Transaction: Transaction

 * <pre>* wrappers (injected at creation time) * + + * | | * +-----------------|--------|--------------+ * | v | | * | +---------------+ | | * | +--| wrapper1 |---|----+ | * | | +---------------+ v | | * | | +-------------+ | | * | | +----| wrapper2 |--------+ | * | | | +-------------+ | | | * | | | | | | * | v v v v | wrapper * | +---+ +---+ +---------+ +---+ +---+ | invariants * perform(anyMethod) | | | | | | | | | | | | maintained * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--------> * | | | | | | | | | | | | * | | | | | | | | | | | | * | | | | | | | | | | | | * | +---+ +---+ +---------+ +---+ +---+ | * | initialize close | * + -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - + *</pre>
Copy the code

According to the above notes, it can be seen that: A Transaction is a method that needs to be executed wrapped in a Wrapper (a set of initialize and close methods is called a wrapper) and executed using the Perform method provided by Transaction.

Before performing, execute the Initialize methods in all wrappers; All close methods are executed after Perform (that is, after method), and Transaction supports multiple wrapper stacks. That’s the transaction mechanism in React.

BatchingStrategy Batch update strategy

Look back to batchingStrategy batch update strategy, ReactDefaultBatchingStrategy transaction is actually a batch update strategy, its wrapper, there are two: FLUSH_BATCHED_UPDATES and RESET_BATCHED_UPDATES.

IsBatchingUpdates are reset to false in the close method with the following code:

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false}},// flushBatchedUpdates merges all temporary states and calculates the latest props and state
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
}

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]
Copy the code

React hook function

When the React hook function updates asynchronously, isBatchingUpdates must be true. By default, isBatchingUpdates must be false. Take a look at the following code:

// ReactMount.js
_renderNewRootComponent: function( nextElement, container, shouldReuseMarkup, context ) {
  // Instantiate the component
  var componentInstance = instantiateReactComponent(nextElement);
  // Call the batchedUpdates method
  ReactUpdates.batchedUpdates(
    batchedMountComponentIntoNode,
    componentInstance,
    container,
    shouldReuseMarkup,
    context
  );
}
Copy the code

This code is a method that is executed when the component is first rendered, and you can see that it internally calls the batchedUpdates method (isBatchingUpdates to true) because the lifecycle (hook) functions are called in sequence during the component’s rendering. If setState is called inside a function, look at the following code:

if(! batchingStrategy.isBatchingUpdates) {// Update the component immediately
  batchingStrategy.batchedUpdates(enqueueUpdate, component)
  return
}
// For batch updates, place components in the dirtyComponents queue first
dirtyComponents.push(component)
Copy the code

Then all updates can go into the dirtyComponents, that is, the asynchronous updates that setState goes through

React synthesis event

When we bind events to the component, setState may also be triggered in the event. To ensure that setState is valid every time, React will also manually enable batch updates here. Look at the following code:

// ReactEventListener.js

dispatchEvent: function (topLevelType, nativeEvent) {
  try {
    // Handle events: batchedUpdates will set isBatchingUpdates to true
    ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
  } finally{ TopLevelCallbackBookKeeping.release(bookKeeping); }}Copy the code

The isBatchingUpdates variable will be set to true before the React lifecycle function and compositing events are executed, so the setState operation will not take effect immediately. When the function completes, the transaction’s close method changes isBatchingUpdates to false.

As in the example above, the entire process simulation looks something like this:

handleClick = () = > {
  // isBatchingUpdates = true
  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) / / 1

  this.setState({
    count: this.state.count + 1,})console.log(this.state.count) / / 1
  // isBatchingUpdates = false
}
Copy the code

And if you have setTimeout intervening

handleClick = () = > {
  // isBatchingUpdates = true
  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) / / 1

  setTimeout(() = > {
    // setTimeout executes asynchronously, at which point isBatchingUpdates have been reset to false
    this.setState({
      count: this.state.count + 1,})console.log(this.state.count) / / 3
  })
  // isBatchingUpdates = false
}
Copy the code

IsBatchingUpdates are changed in synchronized code, whereas setTimeout logic is executed asynchronously. When the this.setState call actually occurs, isBatchingUpdates will already have been reset to false, giving setState in setTimeout the ability to initiate synchronous updates immediately.

BatchedUpdates method

React batchedUpdates is an instable method named unstable_batchedUpdates. In the React batchedUpdates version, this method is named unstable_batchedUpdates.

import React, { Component } from 'react' import { unstable_batchedUpdates as batchedUpdates } from 'react-dom' class App extends Component { state = { count: 1, } handleClick = () => { 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) // 1 setTimeout(() => { batchedUpdates(() => { this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 2})}) render() {return (<> <button onClick={this.handleclick}> add 1</button> <div>{this.state.count}</div> </> ) } } export default AppCopy the code

If the batchedUpdates method is called, the isBatchingUpdates variable will be set to true, and the setTimeout method will be updated asynchronously, so the final print value will be 2. Same result as the first question in this article.

conclusion

SetState asynchrony behaves differently depending on the calling scenario: in React hook functions and synthesized events, it behaves asynchronously; In the case of setTimeout/setInterval functions, DOM native events, it is synchronized. This is determined by the way React transactions and batch updates work.

In React16, the source code is somewhat different due to the introduction of Fiber, but it is similar. I will also write about the principles of React16 later, so stay tuned!


Open source projects I will be maintaining in the near future:

  • Use React + TypeScript + Dumi + Jest + Enzyme to develop UI component libraries
  • Next. Js Enterprise project scaffold template
  • If you feel good, welcome star, give me a little encouragement to continue writing ~