This is the third day of my participation in the August More text Challenge. For details, see:August is more challenging

Based on the latest stable version [email protected], this article introduces the three startup modes of React.

The reconciler process is summed up in four steps in the previous chapter.

This section focuses on the startup process of the React application, which is located in the React-DOM package and connects the input steps in the reconciler’s operational process.

Before formally analyzing the source code, let’s take a look at the react app startup mode:

In the current stable version [email protected] source code, there are three boot methods. Firstly, the introduction of these three modes on the official website is introduced, and the basic description is as follows:

  1. Legacy mode: reactdom.render (
    , rootNode). This is how the React App is currently used. This pattern may not support these new functions (all functions supported by Concurrent).

    // LegacyRoot
    ReactDOM.render(<App />.document.getElementById('root'), dom= > {}); // Callback is supported, and the argument is a DOM object
    Copy the code
  2. Blocking mode: ReactDOM createBlockingRoot (rootNode.) render (< App / >). It is currently being tested and provides only a small part of the functionality of concurrent mode as the first step in migrating to concurrent mode.

    // BolckingRoot
    // 1. Create the ReactDOMRoot object
    const reactDOMBolckingRoot = ReactDOM.createBlockingRoot(
      document.getElementById('root'));// 2. Call render
    reactDOMBolckingRoot.render(<App />); // Callback is not supported
    Copy the code
  3. Concurrent mode: Reactdom.createroot (rootNode).render(
    ). Currently released in V18.0.0-Alpha, and experiment. This mode turns on all the new features.

    // ConcurrentRoot
    // 1. Create the ReactDOMRoot object
    const reactDOMRoot = ReactDOM.createRoot(document.getElementById('root'));
    // 2. Call render
    reactDOMRoot.render(<App />); // Callback is not supported
    Copy the code

Note: Although the source code of 17.0.2 contains createRoot and createBlockingRoot (if you build your own version, you will build experimental version by default), the stable version of the build will exclude these two apis. NPM I React-DOM will not be able to use the API after the 17.0.2 stable version is installed. If you want to play in non-Legacy mode, you need to show the install alpha version (or build it yourself).

Start the process

ReactElement (
) has no association with the DOM object div#root before calling the entry function.

Creating a global object

When React is initialized, it creates three global objects regardless of Legacy, Concurrent, or Blocking mode

  1. ReactDOM(Blocking)Rootobject
  • Belong toreact-domPackage, the objectExposure to haverender,unmountmethods, by calling the instance’srenderMethod to bootstrap the React application.
  1. fiberRootobject

    • Belong toreact-reconcilerPackage, asreact-reconcilerThe global context at run time holds the global state that fiber depends on during the build.
    • Most of its instance variables are used for storageFiber structural cycle(seeTwo big work cyclesThe react application controls execution logic based on the values of these instance variables.
  2. HostRootFiberobject

    • Belong toreact-reconcilerThis is the first Fiber object in the React application. This is the root node of the Fiber tree. The node type isHostRoot.

These three objects are the basic guarantee that the React system works and are not destroyed once created in most scenarios (unless the entire application is unmounted from root.unmount())).

This process is initiated from the React-DOM package, with the React-Reconciler package invoked internally. The core flowchart is as follows (where the creation times of the three objects are marked in red).

The following explains how to create each of these objects one by one.

Create a ReactDOM(Blocking)Root object

Since the apis for the three modes are different, there are three ways to trace from the source code. ReactDOMBlockingRoot = ReactDOMBlockingRoot = ReactDOMBlockingRoot = ReactDOMBlockingRoot = ReactDOMBlockingRoot = ReactDOMBlockingRoot; The type of RootTag determines whether the react application supports interruptible rendering (explained later).

The following analysis is based on the startup functions in the three modes.

Legacy mode

Legacy mode on the surface is called directly ReactDOM. Render, tracking ReactDOM. Render subsequent calls legacyRenderSubtreeIntoContainer links (source)

function legacyRenderSubtreeIntoContainer(parentComponent: ? React$Component<any, any>, children: ReactNodeList, container: Container, forceHydrate: boolean, callback: ?Function.) {
  let root: RootType = (container._reactRootContainer: any);
  let fiberRoot;
  if(! root) {// The first call, root not yet initialized, will enter the branch
    //1. Create the ReactDOMRoot object and initialize the React application environment
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        // Instance finally points to the DOM node generated by children(with input arguments such as 
      )
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // 2. Update the container
    unbatchedUpdates(() = > {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    // Root has been initialized, the second call to render will enter
    // 1. Obtain ReactDOMRoot object
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // 2. Call update
    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  return getPublicRootInstance(fiberRoot);
}
Copy the code

Continue to follow legacyCreateRootFromDOMContainer. Finally, call new ReactDOMBlockingRoot(Container, LegacyRoot, options);

function legacyCreateRootFromDOMContainer(container: Container, forceHydrate: boolean,) :RootType {
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  return createLegacyRoot(
    container,
    shouldHydrate
      ? {
          hydrate: true,} :undefined,); }export function createLegacyRoot(container: Container, options? : RootOptions,) :RootType {
  return new ReactDOMBlockingRoot(container, LegacyRoot, options); // Note that LegacyRoot is fixed, not imported from the outside
}
Copy the code

Through the above analysis, there are two core steps to invoke reactdom. render in Legacy mode:

  1. createReactDOMBlockingRootInstance (analyzed in detail in Concurrent and Blocking modes) to initialize the React application environment.
  2. callupdateContainerUpdate.

Concurrent mode and Blocking mode

Concurrent mode and Blocking mode can be directly seen from the calling mode

  1. Respectively calledReactDOM.createRootandReactDOM.createBlockingRootcreateReactDOMRootandReactDOMBlockingRootThe instance
  2. callReactDOMRootandReactDOMBlockingRootThe instancerendermethods
export function createRoot(container: Container, options? : RootOptions,) :RootType {
  return new ReactDOMRoot(container, options);
}

export function createBlockingRoot(container: Container, options? : RootOptions,) :RootType {
  return new ReactDOMBlockingRoot(container, BlockingRoot, options); // Note that the second argument BlockingRoot is fixed
}
Copy the code

Continue to look at the ReactDOMRoot and ReactDOMBlockingRoot objects

function ReactDOMRoot(container: Container, options: void | RootOptions) {
  // Create a fiberRoot object and mount it on this._internalRoot
  this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
}
function ReactDOMBlockingRoot(
  container: Container,
  tag: RootTag,
  options: void | RootOptions,
) {
  // Create a fiberRoot object and mount it on this._internalRoot
  this._internalRoot = createRootImpl(container, tag, options);
}

ReactDOMRoot.prototype.render = ReactDOMBlockingRoot.prototype.render = function(
  children: ReactNodeList,
) :void {
  const root = this._internalRoot;
  // Perform the update
  updateContainer(children, root, null.null);
};

ReactDOMRoot.prototype.unmount = ReactDOMBlockingRoot.prototype.unmount = function() :void {
  const root = this._internalRoot;
  const container = root.containerInfo;
  // Perform the update
  updateContainer(null, root, null.() = > {
    unmarkContainerAsRoot(container);
  });
};
Copy the code

ReactDOMRoot and ReactDOMBlockingRoot have the same features

  1. callcreateRootImplcreatefiberRootObject and mount it tothis._internalRootOn.
  2. On the prototyperenderandumountMethod, and is called internallyupdateContainerUpdate.

Create a fiberRoot object

In either mode, during the creation of ReactDOM(Blocking)Root, the same function createRootImpl is called, subsequent function calls are viewed, and finally fiberRoot is created (during this process, Pay special attention to the RootTag passing process):

/ / note: three kinds of modes of the tag is different (ConcurrentRoot, BlockingRoot, LegacyRoot).
this._internalRoot = createRootImpl(container, tag, options);
Copy the code
function createRootImpl(
  container: Container,
  tag: RootTag,
  options: void | RootOptions,
) {
  / /... Omit part of the source code (about the hydrate server rendering, etc., temporarily not used)
  // 1. Create fiberRoot
  const root = createContainer(container, tag, hydrate, hydrationCallbacks); // Notice the RootTag passed
  // 2. Tag the DOM object and associate the DOM with the fiber object
  markContainerAsRoot(root.current, container);
  / /... Omit some irrelevant code
  return root;
}
Copy the code
export function createContainer(
  containerInfo: Container,
  tag: RootTag,
  hydrate: boolean,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
) :OpaqueRoot {
  // Create a fiberRoot object
  return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks); // Notice the RootTag passed
}
Copy the code

Create a HostRootFiber object

In createFiberRoot, create the React application’s first fiber object, called HostRootFiber(fiber.tag = HostRoot)

export function createFiberRoot(
  containerInfo: any,
  tag: RootTag,
  hydrate: boolean,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
) :FiberRoot {
  // Create fiberRoot object, notice the RootTag passed
  const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);

  // 1. Here we create the react app's first 'fiber' object, called 'HostRootFiber'
  const uninitializedFiber = createHostRootFiber(tag);
  root.current = uninitializedFiber;
  uninitializedFiber.stateNode = root;
  // 2. Initialize updateQueue for HostRootFiber
  initializeUpdateQueue(uninitializedFiber);

  return root;
}
Copy the code

When creating HostRootFiber, including fiber. The mode attribute, with three RootTag (ConcurrentRoot BlockingRoot, LegacyRoot).

export function createHostRootFiber(tag: RootTag) :Fiber {
  let mode;
  if (tag === ConcurrentRoot) {
    mode = ConcurrentMode | BlockingMode | StrictMode;
  } else if (tag === BlockingRoot) {
    mode = BlockingMode | StrictMode;
  } else {
    mode = NoMode;
  }
  return createFiber(HostRoot, null.null, mode); // Note that the mode attribute is set by RootTag
}
Copy the code

Note: The mode of all nodes in the fiber tree is the same as that of hostRootfiber. mode (the mode of a new fiber node is derived from the parent node), so hostRootfiber. mode is very important because it determines the entire process of building the fiber tree.

At this point, three objects have been created, and the React application has been initialized.

Show the reference of each object in memory at the moment:

  1. legacy

  1. concurrent

  1. blocking

Note:

  1. In the three modes,HostRootFiber.modeIs inconsistent
  2. Legacy,div#rootandReactDOMBlockingRootThrough between_reactRootContainerAssociation. Other patterns are not associated
  3. At this timereactElement(<App/>)It is independent and not yet associated with the three global objects created so far

Call update entry

  1. Legacy backlegacyRenderSubtreeIntoContainerThe function contains:
// 2. Update the container
unbatchedUpdates(() = > {
  updateContainer(children, fiberRoot, parentComponent, callback);
});
Copy the code
  1. Concurrent and blockingReactDOM(Blocking)RootOn the prototyperendermethods
ReactDOMRoot.prototype.render = ReactDOMBlockingRoot.prototype.render = function(
  children: ReactNodeList,
) :void {
  const root = this._internalRoot;
  // Perform the update
  updateContainer(children, root, null.null);
};
Copy the code

Similarities:

  1. All three patterns are executed when the update is invokedupdateContainer. updateContainerThe function is in seriesreact-domwithreact-reconcilerAnd then the logic goes inreact-reconcilerThe package.

Difference:

  1. Updates under Legacy call unbatchedUpdates, change the execution context to LegacyUnbatchedContext, and then call updateContainer for updates.

  2. Concurrent and Blocking do not change the execution context and update is called directly to updateContainer.

Continue tracking the updateContainer function

export function updateContainer(element: ReactNodeList, container: OpaqueRoot, parentComponent: ? React$Component<any, any>, callback: ?Function.) :Lane {
  const current = container.current;
  // 1. Obtain the current timestamp and calculate the priority of this update
  const eventTime = requestEventTime();
  const lane = requestUpdateLane(current);

  // 2. Set fiber.updatequeue
  const update = createUpdate(eventTime, lane);
  update.payload = { element };
  callback = callback === undefined ? null : callback;
  if(callback ! = =null) {
    update.callback = callback;
  }
  enqueueUpdate(current, update);

  // 3. Enter the 'input' section of the reconcier operation process
  scheduleUpdateOnFiber(current, lane, eventTime);
  return lane;
}
Copy the code

The updateContainer function is in the React-Reconciler package, which concatenates the React-dom with the React-Reconciler. Instead of going into the details of the updateContainer function, you’ll notice that it finally calls scheduleUpdateOnFiber.

In the previous reconciler operation process, scheduleUpdateOnFiber is the entry function of the input phase.

So, by calling the React-DOM API (e.g., reactdom.render), the React interior goes through a series of operations to complete the initialization and enter the first phase of the Reconciler’s process.

thinking

Interruptible rendering

React’s most well-known interruptible render (render can be interrupted, some lifecycle functions can be executed multiple times, UNSAFE_componentWillMount, UNSAFE_componentWillReceiveProps) only in HostRootFiber mode = = = ConcurrentRoot | BlockingRoot will open. Reactdom.render (
, dom); In this case, both the first Render and subsequent updates only enter the synchronization work loop, and reconciliation has no chance to break, so the lifecycle function is only called once.

The hype for interruptible rendering first came from a 2017 speech by Lin Clark. In the speech, it is explained that React will use fiber architecture in the future, and reconciliation can be interrupted (13:15 seconds). Fiber was applied in V16.1.0.

In the latest stable version v17.0.2, interruptible rendering is implemented, but the API is not exposed in the stable version. You can only experience this feature by installing the alpha version.

But many developers thought that the stable version of React was already interruptible (which is a mistake), probably because of the hype. The front environment is still more impetuous, at present, more need to calm down to learn.

conclusion

This section describes the three startup modes of the React application. Three key objects created after startup are analyzed, and the reference relationship of the objects in memory is drawn. The startup process finally calls updateContainer into the React-Reconciler package, which in turn calls the schedulerUpdateOnFiber function to connect to the input phase of the reconciler’s operational process.

Write in the last

This article belongs to the diagram react source code series in the operation of the core plate, this series of nearly 20 articles, really in order to understand the React source code, and then improve the architecture and coding ability.

The first draft of the graphic section has been completed and will be updated in August. If there are any errors in the article, we will correct them as soon as possible on Github.