React fiber is the latest fiber architecture in the React system. The fiber architecture of React is the latest fiber architecture in the React system. React source code is much more complex than I imagined. Therefore, I plan to analyze it in several modules. Today, I will first talk about the evolution process of React update strategy from synchronous to asynchronous.

From the setState

React 16 had to undergo a major refactoring because the previous React version had some inevitable bugs, some updates that needed to be changed from synchronous to asynchronous. So let’s talk about how React 15 performs a setState.

import React from 'react';

class App extends React.Component {
  state = { val: 0 }
  componentDidMount() {
    // First call
    this.setState({ val: this.state.val + 1 });
    console.log('first setState'.this.state);

    // Second call
    this.setState({ val: this.state.val + 1 });
    console.log('second setState'.this.state);

    // Third call
    this.setState({ val: this.state.val + 1 }, () = > {
      console.log('in callback'.this.state)
    });
  }
  render() {
    return <div> val: { this.state.val } </div>}}export default App;
Copy the code

If you are familiar with React, you should know that multiple setstates are merged into one during the React lifecycle. Although setstates are performed three times in succession, the value of state.val is actually recalculated only once.

Every time after setState, immediately fetching state will find that there is no update. The latest result is only available in the setState callback function, which is confirmed by the output from the console.

There are many articles on the Internet that say setState is an “asynchronous operation”, so setState cannot obtain the latest value after the operation. In fact, this view is wrong. SetState is a synchronous operation, but it is not executed immediately after each operation. Instead, setState is cached. After the mount process or event operation, all the states will be taken out for a calculation. If setState falls out of the React lifecycle or the event stream provided by React, setState gets results immediately afterwards.

We modify the above code to put setState in setTimeout and execute on the next task queue.

import React from 'react';

class App extends React.Component {
  state = { val: 0 }
  componentDidMount() {
    setTimeout(() = > {
      // First call
      this.setState({ val: this.state.val + 1 });
      console.log('first setState'.this.state);
  
      // Second call
      this.setState({ val: this.state.val + 1 });
      console.log('second setState'.this.state);
    });
  }
  render() {
    return <div> val: { this.state.val } </div>}}export default App;
Copy the code

As you can see, immediately after setState, you can see that the value of state.val has changed.

React 15 setState update logic To understand the setState update logic in React 15, the following code is a simplification of the source code.

Old version setState source code analysis

The main logic of setState is implemented in ReactUpdateQueue. After calling setState, the state is not changed immediately. Instead, the parameters passed in are placed in the _pendingStateQueue within the component. EnqueueUpdate is then called to update.

// Exposed React.Component
function ReactComponent() {
  this.updater = ReactUpdateQueue;
}
// The setState method is mounted to the prototype chain
ReactComponent.prototype.setState = function (partialState, callback) {
  // After calling setState, the internal updater.enqueuesetState is called
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState'); }};var ReactUpdateQueue = {
  enqueueSetState(component, partialState) {
    // Temporarily store the new state on the component's _pendingStateQueue
    if(! component._pendingStateQueue) { component._pendingStateQueue = []; }var queue = component._pendingStateQueue;
    queue.push(partialState);
    enqueueUpdate(component);
  },
  enqueueCallback: function (component, callback, callerName) {
    // Hold callback temporarily on the component's _pendingCallbacks
    if (component._pendingCallbacks) {
      component._pendingCallbacks.push(callback);
    } else{ component._pendingCallbacks = [callback]; } enqueueUpdate(component); }}Copy the code

EnqueueUpdate will first through batchingStrategy. IsBatchingUpdates judge whether the current in the update process, if not in the update process, will be called batchingStrategy. BatchedUpdates update (). If in the process, components to be updated are cached in dirtyComponents.

var dirtyComponents = [];
function enqueueUpdate(component) {
  if(! batchingStrategy.isBatchingUpdates) {// Start batch update
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // If in the update process, the component is put into the dirty component queue, indicating that the component is waiting to be updated
  dirtyComponents.push(component);
}
Copy the code

React batchingStrategy batchingStrategy is a strategy for batch processing implemented by React. The strategy is based on Transaction, which has the same name as a database Transaction but does different things.

class ReactDefaultBatchingStrategyTransaction extends Transaction {
  constructor() {
    this.reinitializeTransaction()
  }
  getTransactionWrappers () {
    return[{initialize: () = > {},
        close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
      },
      {
        initialize: () = > {},
        close: () = > {
          ReactDefaultBatchingStrategy.isBatchingUpdates = false; }}]}}var transaction = new ReactDefaultBatchingStrategyTransaction();

var batchingStrategy = {
  // Check whether the update process is in progress
  isBatchingUpdates: false.// Start batch update
  batchedUpdates: function (callback, component) {
    // Get the status of the previous update
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
		// Change the update status to true
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    if (alreadyBatchingUpdates) {
      // If you are already in the update state, wait for the previous update to end
      return callback(callback, component);
    } else {
      // Update
      return transaction.perform(callback, null, component); }}};Copy the code

Transaction is started with the Perform method, and the extended getTransactionWrappers method retrieves an array of Wrapper objects, each containing two properties: Initialize and CLOSE. Perform calls all wrapper.initialize, incoming callbacks, and all wrapper.close.

class Transaction {
	reinitializeTransaction() {
    this.transactionWrappers = this.getTransactionWrappers();
  }
	perform(method, scope, ... param) {
    this.initializeAll(0);
    varret = method.call(scope, ... param);this.closeAll(0);
    return ret;
  }
	initializeAll(startIndex) {
    var transactionWrappers = this.transactionWrappers;
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      wrapper.initialize.call(this); }}closeAll(startIndex) {
    var transactionWrappers = this.transactionWrappers;
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      wrapper.close.call(this); }}}Copy the code

The React source code comments also graphically illustrate this process.

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

Let’s simplify the code and revisit the flow of setState.

// 1. Call component.setstate
ReactComponent.prototype.setState = function (partialState) {
  this.updater.enqueueSetState(this, partialState);
};

/ / 2. Call ReactUpdateQueue. EnqueueSetState, put the state value in _pendingStateQueue caching
var ReactUpdateQueue = {
  enqueueSetState(component, partialState) {
    varqueue = component._pendingStateQueue || (component._pendingStateQueue = []); queue.push(partialState); enqueueUpdate(component); }}// 3. Check whether the update is in progress. If not, update it
var dirtyComponents = [];
function enqueueUpdate(component) {
  // isBatchingUpdates must be false if they were not updated before
  if(! batchingStrategy.isBatchingUpdates) {/ / call batchingStrategy. BatchedUpdates updated
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  dirtyComponents.push(component);
}

// 4. Perform the update, and put the update logic into the transaction for processing
var batchingStrategy = {
  isBatchingUpdates: false.// Note that callback is enqueueUpdate
  batchedUpdates: function (callback, component) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    if (alreadyBatchingUpdates) {
      // If you are already updating, re-call enqueueUpdate to put Component in dirtyComponents
      return callback(callback, component);
    } else {
      // Perform transaction operations
      return transaction.perform(callback, null, component); }}};Copy the code

The startup transaction can be broken down into three steps:

  1. Initialize of the wrapper is performed first. Initialize is empty and can be skipped.
  2. Callback (also known as enqueueUpdate) is executed, and enqueueUpdate is executed because it is already in the update state.batchingStrategy.isBatchingUpdatesHas been modified totrue, so the component will end up in the dirty component queue waiting for updates.
  3. The next two close methods, the firstflushBatchedUpdatesIs used to perform component updates, and the second method is used to modify the update status, indicating that the update has ended.
getTransactionWrappers () {
  return[{initialize: () = > {},
      close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
    },
    {
      initialize: () = > {},
      close: () = > {
        ReactDefaultBatchingStrategy.isBatchingUpdates = false; }}}]Copy the code

FlushBatchedUpdates diff all dirty component queues and then update them to the DOM.

function flushBatchedUpdates() {
  if (dirtyComponents.length) {
    runBatchedUpdates()
  }
};

function runBatchedUpdates() {
  // Omit some de-redo and sort operations
  for (var i = 0; i < dirtyComponents.length; i++) {
    var component = dirtyComponents[i];

    // Determine if the component needs to be updated, then diff, and finally update the DOM.ReactReconciler.performUpdateIfNecessary(component); }}Copy the code

PerformUpdateIfNecessary () invokes the Component. UpdateComponent (), in updateComponent (), remove all values from _pendingStateQueue to update.

// Get the latest state
_processPendingState() {
  var inst = this._instance;
  var queue = this._pendingStateQueue;

  varnextState = { ... inst.state };for (var i = 0; i < queue.length; i++) {
    var partial = queue[i];
    Object.assign(
      nextState,
      typeof partial === 'function' ? partial(inst, nextState) : partial
   );
  }
  return nextState;
}
// Update the component
updateComponent(prevParentElement, nextParentElement) {
  var inst = this._instance;
  var prevProps = prevParentElement.props;
  var nextProps = nextParentElement.props;
  var nextState = this._processPendingState();
  varshouldUpdate = ! shallowEqual(prevProps, nextProps) || ! shallowEqual(inst.state, nextState);if (shouldUpdate) {
    // diff 、update DOM
  } else {
    inst.props = nextProps;
    inst.state = nextState;
  }
  // Subsequent operations include determining whether the component needs to be updated, diff, and updated to the DOM
}
Copy the code

SetState Merge reason

According to the logic of just explain setState, batchingStrategy. IsBatchingUpdates to false opens a transaction, the component into the dirty component queue, the last update operations, and here is synchronous operation. So, reasonably, after setState, we can immediately get the latest state.

This is not the case, however, in the life cycle and the flow of events of the React, batchingStrategy. IsBatchingUpdates values have been modified became true. Take a look at the following two images:

BatchedUpdates are called when component mounts and events are called, and the transaction has already started, so setState will put its component in the dirty component queue for updates no matter how many times it stays in React. Once unmanaged by React, such as in setTimeout, setState immediately becomes a one-man operation.

Concurrent mode

React 16 introduced Fiber architecture to pave the way for asynchronous rendering capability. Although the architecture has been switched, asynchronous rendering capability is not officially available, so we can only use it in the experimental version. Asynchronous rendering refers to Concurrent mode, as described below:

Concurrent mode is a new feature in React that helps applications stay responsive and adjust appropriately to the user’s device performance and network speed.

In addition to Concurrent mode, React also provides two other modes. Legacy mode is still a synchronous update mode and can be considered as a consistent compatibility mode with older versions, while Blocking mode is a transitional version.

Concurrent mode simply means that component updates are asynchronized, split into timeslices, and scheduling, diff, and updates are performed only in the specified timeslices before rendering. If timed out, they are suspended and moved to the next timeslice, giving the browser a breathing space.

Browsers are single-threaded and put together GUI rendering, timer handling, event handling, JS execution, and remote resource loading. When you do something, you have to finish it before you can do the next thing. If we have enough time, the browser will JIT and hot optimize our code, and some DOM operations will be handled internally with reflow. Reflow is a performance black hole, and most elements of the page are likely to be rearranged.

Browser processes: rendering – > tasks – > render – > tasks – > render – >…

Some of these tasks are controllable and some are not. For example, it is hard to say when setTimeout will be executed. It is always inaccurate. The resource loading time is not controllable. However, we can control some JS, let them dispatch execution, tasks should not be too long, so that the browser has time to optimize JS code and fix reflow!

In summary, give your browser a good rest and it will run faster.

— React Fiber Architecture by James Seto

Here is a demo of 🌟 running around ☀️. Here is the update view of React timing setState. In synchronous mode, each setState will cause the animation to stall, while in asynchronous mode, the animation will run smoothly.

Synchronous mode:

Asynchronous mode:

How to use

While many articles have been written about Concurrent mode, this capability is not actually live and can only be used by installing experimental versions. You can also use this CDN directly: unpkg.com/browse/reac… .

npm install react@experimental react-dom@experimental
Copy the code

To enable Concurrent mode, reactdom. render should be replaced with reactdom. createRoot. In the experimental version, due to unstable API, The Concurrent mode needs to be enabled by reactdom.unstable_createroot.

import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.unstable_createRoot(
  document.getElementById('root')
).render(<App />);
Copy the code

SetState merge update

Remember that in the case of React15, setTimeout used setState, and the value of state.val immediately changed. We run the same code in Concurrent mode once.

import React from 'react';

class App extends React.Component {
  state = { val: 0 }
  componentDidMount() {
    setTimeout(() = > {
      // First call
      this.setState({ val: this.state.val + 1 });
      console.log('first setState'.this.state);
  
      // Second call
      this.setState({ val: this.state.val + 1 });
      console.log('second setState'.this.state);
      
      this.setState({ val: this.state.val + 1 }, () = > {
        console.log(this.state);
      });
    });
  }
  render() {
    return <div> val: { this.state.val } </div>}}export default App;
Copy the code

Note That in Concurrent mode, setState can merge updates even if it leaves the React lifecycle. The main reason is that in Concurrent mode, the actual update operations are moved to the next event queue, similar to Vue’s nextTick.

Update mechanism changes

Let’s modify the demo and look at the call stack after the button is clicked.

import React from 'react';

class App extends React.Component {
  state = { val: 0 }
  clickBtn() {
    this.setState({ val: this.state.val + 1 });
  }
  render() {
    return (<div>
      <button onClick={()= > {this.clickBtn()}}>click add</button>
      <div>val: { this.state.val }</div>
    </div>)}}export default App;
Copy the code

Once onClick is triggered, we do setState and then we call enquueState, and it looks like the enquueState mode is the same as it was before, but everything else has changed, because there are no transactions in React 16 anymore.

Component.setState() => enquueState() => scheduleUpdate() => scheduleCallback()
=> requestHostCallback(flushWork) => postMessage()
Copy the code

The real asynchronous logic is in the requestHostCallback, postMessage, and React scheduler: github.com/facebook/re… .

function unstable_scheduleCallback(priorityLevel, calback) {
  var currentTime = getCurrentTime();
  var startTime = currentTime + delay;
  var newTask = {
    id: taskIdCounter++,
    startTime: startTime,           // Task start time
    expirationTime: expirationTime, // Task termination time
    priorityLevel: priorityLevel,   // Scheduling priority
    callback: callback,             // The callback function
  };
  if (startTime > currentTime) {
    // The task will be placed in the taskQueue and executed in the next time slice
    // timerQueue is a task from timerQueue to taskQueue
  	push(taskQueue, newTask)
  } else {
    requestHostCallback(flushWork);
  }
  return newTask;
}
Copy the code

The implementation of requestHostCallback relies on MessageChannel, but instead of doing messaging, MessageChannel takes advantage of its asynchronous capabilities to give the browser a break. Speaking of MessageChannel, nextTick was also used in Vue 2.5, but was removed in 2.6.

MessageChannel exposes two objects, Port1 and Port2. The message sent by port1 can be received by Port2, and the message sent by Port2 can be received by Port1, except that the time when the message is received will be placed in the next macroTask.

var { port1, port2 } = new MessageChannel();
Port1 receives messages from port2
port1.onmessage = function (msg) { console.log('MessageChannel exec')}// port2 sends messages
port2.postMessage(null)

new Promise(r= > r()).then(() = > console.log('promise exec'))
setTimeout(() = > console.log('setTimeout exec'))

console.log('start run')
Copy the code

As you can see, port1 receives the message later than the microTask where the Promise resides, but earlier than setTimeout. React takes advantage of this ability to give the browser a breathing space before it dies of starvation.

As in the previous case, synchronizing updates didn’t give the browser any respite, causing the view to lag.

Asynchronous updates split the time slice, giving the browser enough time to update the animation.

Back at the code level, see how React leverages MessageChannel.

var isMessageLoopRunning = false; // Update the status
var scheduledHostCallback = null; // Global callback
var channel = new MessageChannel();
var port = channel.port2;

channel.port1.onmessage = function () {
  if(scheduledHostCallback ! = =null) {
    var currentTime = getCurrentTime();
    // Reset the timeout period
    deadline = currentTime + yieldInterval;
    var hasTimeRemaining = true;

    / / callback execution
    var hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);

    if(! hasMoreWork) {// There is no task left, change the status
      isMessageLoopRunning = false;
      scheduledHostCallback = null;
    } else {
      // There are tasks to be executed in the next task queue to give the browser a chance to breathe
      port.postMessage(null); }}else {
    isMessageLoopRunning = false; }}; requestHostCallback =function (callback) {
  // Callback is mounted to scheduledHostCallback
  scheduledHostCallback = callback;

  if(! isMessageLoopRunning) { isMessageLoopRunning =true;
    // Push a message, and the next queue queue calls callback
    port.postMessage(null); }};Copy the code

Callback (flushWork) is called workLoop to retrieve the task execution from the taskQueue.

// Quite a bit of code has been streamlined
function flushWork(hasTimeRemaining, initialTime) {
  return workLoop(hasTimeRemaining, initialTime);
}

function workLoop(hasTimeRemaining, initialTime) {
  var currentTime = initialTime;
  // scheduleCallback performs a taskQueue push
  // Here is the unperformed operation of the previous time slice
  currentTask = peek(taskQueue);

  while(currentTask ! = =null) {
    if (currentTask.expirationTime > currentTime) {
      // The task is interrupted due to timeout
      break;
    }

    currentTask.callback();         // Perform the task callback
    currentTime = getCurrentTime(); // Reset the current time
    currentTask = peek(taskQueue);  // Get a new task
  }
	// If the current task is not empty, timeout is interrupted and true is returned
  if(currentTask ! = =null) {
    return true;
  } else {
    return false; }}Copy the code

React expirationTime = expirationTime = expirationTime = expirationTime = expirationTime Therefore, in the asynchronous model setTimeout for setState, as long as the currentTime slice does not end (currentTime is less than expirationTime), you can still merge multiple setstates into one.

Let’s do another experiment. In setTimeout, setState 500 times in a row to see how many times it takes effect.

import React from 'react';

class App extends React.Component {
  state = { val: 0 }
  clickBtn() {
    for (let i = 0; i < 500; i++) {
      setTimeout(() = > {
        this.setState({ val: this.state.val + 1}); }}})render() {
    return (<div>
      <button onClick={()= > {this.clickBtn()}}>click add</button>
      <div>val: { this.state.val }</div>
    </div>)}}export default App;
Copy the code

Let’s take a look at synchronous mode:

Let’s look at asynchronous mode:

The final setState count is 81, indicating that the operation was performed in 81 time slices, each of which was updated once.

conclusion

It took a long time before and after this article. It was really painful to read the source code of React, because I didn’t know it before. At first, I read the analysis of some articles, but there were many ambiguations, so I had to debug the source code. I feel like I’m running out of ideas.

Of course, this article only briefly introduces the update mechanism from synchronous to asynchronous. In fact, React 16 updates not only asynchronously, but also has many details about time slice division and task priority. These things will be discussed in the next article.