Introduction
stay Last article We quickly build a simple example through the create react app scaffold, and based on this example, we explain the implementation principle behind React.Component and React.PureComponent in class components. At the same time, we also learned that by using the Babel preset toolkit @ Babel / preset react, the return value of the render method and the return value of the function definition component in the class component can be transformed into a multi-layer nested structure wrapped by the React.createElement method. Based on the source code, the implementation process behind the React.createElement method and the members of the ReactElement constructor are analyzed line by line Finally, according to the results of the analysis, it summarizes several interview sites that may be encountered in the interview or that you have encountered before. The content in the previous article is relatively simple, which is mainly to lay the foundation for this article and the subsequent task scheduling related content, helping us better understand the intention of the source code. Based on the basic content of the previous article, this article starts with the method of ReactDOM.render, which is the entry point of component rendering. Step by step, it goes deep into the source code, uncovering the implementation principle behind the method of ReactDOM.render. If there is any error, please point out.
In the source code, there are many control statements that judge the variables similar to "DEV" to distinguish the development environment and the production environment. The author doesn't care much about these contents in the process of reading the source code, so he directly ignores them. Interested partners can study and research by themselves.
render VS hydrate
The source code analysis of this series is based on the version of react v16.10.2. In order to ensure the source code is consistent or you are recommended to choose the same version, the download address and the specific reasons for the author to choose this version can be viewed in the section of preparation stage of the previous article, which will not be explained too much here. The project example itself is also relatively simple. You can use the create react app to quickly build a simple example according to the steps in the preparation stage. Then we can locate it in the src/index.js file and see the following code:
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; ... ReactDOM.render(<App />, document.getElementById('root')); ...
This file is the main entry file of the project, App component is the root component, and react dom.render is the entry point where we start to analyze the source code. We can find the complete code of the ReactDOM object through the following path:
packages -> react-dom -> src -> client -> ReactDOM.js
Then we navigate to line 632 and see that the ReactDOM object contains many methods that we may have used, such as render, createPortal, findDOMNode, hydrate and unmountComponentAtNode. In this paper, we only care about render method for the moment, but for the convenience of comparison, we can also simply look at the hydrate method:
const ReactDOM: Object = { ... /** * Server rendering * @param element Represents a ReactNode, which can be a ReactElement object * @param container Need to mount component to DOM container in page * @param callback Callback function to be executed after rendering */ hydrate(element: React$Node, container: DOMContainer, callback: ?Function) { invariant( isValidContainer(container), 'Target container is not a DOM element.', ); ... // TODO: throw or warn if we couldn't hydrate? // Note that the first parameter is null and the fourth parameter is true return legacyRenderSubtreeIntoContainer( null, element, container, true, callback, ); }, /** * Client rendering * @param element Represents a ReactElement object * @param container Need to mount component to DOM container in page * @param callback Callback function to be executed after rendering */ render( element: React$Element<any>, container: DOMContainer, callback: ?Function, ) { invariant( isValidContainer(container), 'Target container is not a DOM element.', ); ... // Note that the first parameter is null and the fourth parameter is false return legacyRenderSubtreeIntoContainer( null, element, container, false, callback, ); }, ... };
I found that the first parameter of the render method is the ReactElement object we mentioned in the previous article, so the content of the previous article is to lay a foundation for us to understand the parameter. In fact, the element field in almost all method parameters in the source code can be passed into a ReactElement instance, which is obtained by using the React.createElement method in the compilation process of the Babel compiler. Next, we call legacyRenderSubtreeIntoContainer in the render method to formally enter the rendering process. However, it is important to note that when the render method and the hydrate method perform legacyRenderSubtreeIntoContainer, the values of the first parameter are all null, and the values of the fourth parameters are exactly the opposite.
Then navigate to line 570 and enter the specific implementation of legacyRenderSubtreeIntoContainer method:
/** * Start building the FiberRoot and RootFiber, and then start the update task * @param parentComponent Parent component, which can be treated as null value * @param children ReactDOM.render()Or the first parameter in react dom. Draft(), which can be understood as the root component * @param container ReactDOM.render()Or the second parameter in react dom. Draft(), the DOM container that the component needs to mount * @param forceHydrate Indicates whether to merge. It is used to distinguish between client-side rendering and server-side rendering. The render method passes false, and the draft method passes true * @param callback ReactDOM.render()Or the third parameter in reactdom. Draft(), which is the callback function to be executed after the component rendering is completed * @returns {*} */ function legacyRenderSubtreeIntoContainer( parentComponent: ?React$Component<any, any>, children: ReactNodeList, container: DOMContainer, forceHydrate: boolean, callback: ?Function, ) { ... // TODO: Without `any` type, Flow says "Property cannot be accessed on any // member of intersection type." Whyyyyyy. // At the first execution, there must be no "react rootcontainer" property on the container // So the first time you execute it, root must be undefined let root: _ReactSyncRoot = (container._reactRootContainer: any); let fiberRoot; if (!root) { // Initial mount // It is the first time to mount and enter the current process control. The container. Reactrotcontainer points to a ReactSyncRoot instance root = container._reactRootContainer = legacyCreateRootFromDOMContainer( container, forceHydrate, ); // Root represents a ReactSyncRoot instance, in which an internal root method points to a fiberRoot instance fiberRoot = root._internalRoot; // callback represents the third parameter in ReactDOM.render() or reactdom. Draft() // Rewrite the callback, find the corresponding rootFiber through the fiberRoot, and then point the stateNode of the first child of the rootFiber as this in the callback // In general, we seldom write the third parameter, so we don't need to worry about the content here if (typeof callback === 'function') { const originalCallback = callback; callback = function() { const instance = getPublicRootInstance(fiberRoot); originalCallback.call(instance); }; } // Initial mount should not be batched. // For the first mount, the update operation should not be batch, so the unbatchedUpdates method will be executed first // In this method, the execution context will be switched to legacy unbatchedcontext // Call updateContainer to perform update operation after context switching // After executing the updateContainer, restore the executionContext to its previous state unbatchedUpdates(() => { updateContainer(children, fiberRoot, parentComponent, callback); }); } else { // It is not the first time to mount, i.e. there is already an instance of ReactSyncRoot on the container fiberRoot = root._internalRoot; // The following control statement is consistent with the above logic if (typeof callback === 'function') { const originalCallback = callback; callback = function() { const instance = getPublicRootInstance(fiberRoot); originalCallback.call(instance); }; } // Update // For non first mount, there is no need to call the unbatchedUpdates method // That is, it is no longer necessary to switch execution context to legacy unbatchedcontext // Instead, the updateContainer is called directly to perform the update operation updateContainer(children, fiberRoot, parentComponent, callback); } return getPublicRootInstance(fiberRoot); }
The content of the above code is a little bit too much. It may not be easy to understand at first glance. For the moment, we don't have to worry about the whole function content. Imagine the first time we start a running project, that is, when we execute the ReactDOM.render method for the first time, we need to get the container. The reactrootcontainer must have no value, so we first care about the content of the first if statement:
if (!root) { // Initial mount // It is the first time to mount and enter the current process control. The container. Reactrotcontainer points to a ReactSyncRoot instance root = container._reactRootContainer = legacyCreateRootFromDOMContainer( container, forceHydrate, ); ... }
Here, we call the legacyCreateRootFromDOMContainer method to assign its return value to the container. \
/** * Create and return a ReactSyncRoot instance * @param container ReactDOM.render()Or the second parameter in react dom. Draft(), the DOM container that the component needs to mount * @param forceHydrate Whether forced fusion is required, the render method passes false, and the draft method passes true * @returns {ReactSyncRoot} */ function legacyCreateRootFromDOMContainer( container: DOMContainer, forceHydrate: boolean, ): _ReactSyncRoot { // Judge whether fusion is needed const shouldHydrate = forceHydrate || shouldHydrateDueToLegacyHeuristic(container); // First clear any existing content. // In the case of client rendering, all elements in the container need to be removed if (!shouldHydrate) { let warned = false; let rootSibling; // Loop through each child node to delete while ((rootSibling = container.lastChild)) { ... container.removeChild(rootSibling); } } ... // Legacy roots are not batched. // Return a ReactSyncRoot instance // This instance has an "internalRoot" attribute pointing to the fiberRoot return new ReactSyncRoot( container, LegacyRoot, shouldHydrate ? { hydrate: true, } : undefined, ); } /** * Judge whether fusion is needed according to nodeType and attribute * @param container DOM container * @returns {boolean} */ function shouldHydrateDueToLegacyHeuristic(container) { const rootElement = getReactRootElementInContainer(container); return !!( rootElement && rootElement.nodeType === ELEMENT_NODE && rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME) ); } /** * Get the first child node in the DOM container according to the container * @param container DOM container * @returns {*} */ function getReactRootElementInContainer(container: any) { if (!container) { return null; } if (container.nodeType === DOCUMENT_NODE) { return container.documentElement; } else { return container.firstChild; } }
In the shouldHydrateDueToLegacyHeuristic method, the first sub node in the DOM container is obtained according to the container. The purpose of obtaining the sub node is to distinguish between client-side rendering and service-side rendering by nodeType and whether the node has the root'attribute'name attribute. The root'attribute'name is located in the packages / react DOM / SRC / shared / domproperty.js file, Represents the data react root attribute. We know that in the rendering on the server side, unlike in the rendering on the client side, the node service will first generate a complete HTML string according to the matching route in the background, and then send the HTML string to the browser side. The resulting HTML structure is simplified as follows:
<body> <div id="root"> <div data-reactroot=""></div> </div> </body>
There is no data React root attribute in client-side rendering, so you can distinguish between client-side rendering and server-side rendering. There are five nodetypes in React. Their corresponding values are consistent with the nodeType standard in W3C. They are located in the HTMLNodeType.js file at the same level as DOMProperty.js
// Representative element node export const ELEMENT_NODE = 1; // Represent text node export const TEXT_NODE = 3; // On behalf of annotation node export const COMMENT_NODE = 8; // Represents the entire document, i.e. document export const DOCUMENT_NODE = 9; // Represent document fragment node export const DOCUMENT_FRAGMENT_NODE = 11;
After the above analysis, now we can easily distinguish between client-side rendering and server-side rendering, and if we are asked about the difference between the two rendering modes in the interview, we can easily say the implementation difference between the two at the source level, so that the interviewer can see the difference. So far, I think it's quite simple, isn't it?
FiberRoot VS RootFiber
In this section, we will try to understand two more confusing concepts: FiberRoot and RootFiber. These two concepts play a key role in the whole task scheduling process of React. If we don't understand these two concepts, the subsequent task scheduling process is empty talk, so here is also the part we must understand. Next, we will continue to analyze the rest of the legacyCreateRootFromDOMContainer method, and return a ReactSyncRoot instance at the end of the function body. We can easily find the concrete content of the ReactSyncRoot constructor by returning to the ReactDOM.js file:
/** * ReactSyncRoot Constructor * @param container DOM container * @param tag fiberRoot Tags for nodes (LegacyRoot, BatchedRoot, ConcurrentRoot) * @param options The configuration information has a value only when it is draft, otherwise it is undefined * @constructor */ function ReactSyncRoot( container: DOMContainer, tag: RootTag, options: void | RootOptions, ) { this._internalRoot = createRootImpl(container, tag, options); } /** * Create and return a fiberRoot * @param container DOM container * @param tag fiberRoot Tags for nodes (LegacyRoot, BatchedRoot, ConcurrentRoot) * @param options The configuration information has a value only when it is draft, otherwise it is undefined * @returns {*} */ function createRootImpl( container: DOMContainer, tag: RootTag, options: void | RootOptions, ) { // Tag is either LegacyRoot or Concurrent Root // Determine whether it is a hydrate mode const hydrate = options != null && options.hydrate === true; const hydrationCallbacks = (options != null && options.hydrationOptions) || null; // Create a fiberRoot const root = createContainer(container, tag, hydrate, hydrationCallbacks); // Attach an internal attribute to the container to point to the rootFiber node corresponding to the current attribute of the fiberRoot markContainerAsRoot(root.current, container); if (hydrate && tag !== LegacyRoot) { const doc = container.nodeType === DOCUMENT_NODE ? container : container.ownerDocument; eagerlyTrapReplayableEvents(doc); } return root; }
From the above source code, we can see that the createRootImpl method creates a fiberRoot instance by calling the createContainer method, returns the instance and assigns it to the internal member ﹐ internalRoot property of the ReactSyncRoot constructor. Let's go on to the createContainer method to explore the whole creation process of the fiberRoot. The method is extracted into another related dependency package, the react reconciler package, which is the same as the react DOM package, and then locate line 299 of react reconciler / SRC / react fiberreconciler.js:
/** * An internal call to the createFiberRoot method returns a fiberRoot instance * @param containerInfo DOM container * @param tag fiberRoot Tags for nodes (LegacyRoot, BatchedRoot, ConcurrentRoot) * @param hydrate Determine whether it is a hydrate mode * @param hydrationCallbacks Values are possible only in the hydrate mode, which contains two optional methods: onhydrarated and onDeleted * @returns {FiberRoot} */ export function createContainer( containerInfo: Container, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, ): OpaqueRoot { return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks); } /** * Create and reference fiberRoot and rootFiber to each other * @param containerInfo DOM container * @param tag fiberRoot Tags for nodes (LegacyRoot, BatchedRoot, ConcurrentRoot) * @param hydrate Determine whether it is a hydrate mode * @param hydrationCallbacks Values are possible only in the hydrate mode, which contains two optional methods: onhydrarated and onDeleted * @returns {FiberRoot} */ export function createFiberRoot( containerInfo: any, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, ): FiberRoot { // Create an instance of fiberRoot through the FiberRootNode constructor const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any); if (enableSuspenseCallback) { root.hydrationCallbacks = hydrationCallbacks; } // Cyclic construction. This cheats the type system right now because // stateNode is any. // Create the root node of the fiber tree through the createHostRootFiber method, that is, rootFiber // It should be noted that the fiber node will also form a single chain table tree structure like the DOM tree structure // Each DOM node or component will generate a corresponding fiber node (the generation process will be interpreted in the following articles) // It plays an important role in the subsequent reconciliation stage const uninitializedFiber = createHostRootFiber(tag); // After the rootFiber is created, the current property of the fiberRoot instance will be pointed to the rootFiber just created root.current = uninitializedFiber; // At the same time, the stateNode property of rootFiber points to the instance of fiberRoot, forming a mutual reference uninitializedFiber.stateNode = root; // Finally, return the created fiberRoot instance return root; }
A complete instance of the FiberRootNode contains many useful properties, which play their respective roles in the task scheduling phase. You can see the implementation of the full FiberRootNode constructor in the ReactFiberRoot.js file (only some properties are listed here):
/** * FiberRootNode Constructor * @param containerInfo DOM container * @param tag fiberRoot Tags for nodes (LegacyRoot, BatchedRoot, ConcurrentRoot) * @param hydrate Determine whether it is a hydrate mode * @constructor */ function FiberRootNode(containerInfo, tag, hydrate) { // Type used to mark the fiberRoot this.tag = tag; // Point to the currently active rootFiber node corresponding to it this.current = null; // Information about the DOM container associated with the fiberRoot this.containerInfo = containerInfo; ... // Whether the current fiberRoot is in hydrate mode this.hydrate = hydrate; ... // Only one task will be maintained on each fiberRoot instance, which is saved in the callbackNode property this.callbackNode = null; // Priority of the current task this.callbackPriority = NoPriority; ... }
Some attribute information is as shown above. Because there are too many attributes and they are not available in this article, we will not list them one by one. The remaining attributes and their annotation information have been uploaded to Github , interested friends can check it by themselves. After understanding the property structure of the fiberRoot, we will continue to explore the second half of the createFiberRoot method:
// The following code comes from the createFiberRoot method above // Create the root node of the fiber tree through the createHostRootFiber method, that is, rootFiber const uninitializedFiber = createHostRootFiber(tag); // After the rootFiber is created, the current property of the fiberRoot instance will be pointed to the rootFiber just created root.current = uninitializedFiber; // At the same time, the stateNode property of rootFiber points to the instance of fiberRoot, forming a mutual reference uninitializedFiber.stateNode = root; // The following code is from the ReactFiber.js file /** * Internal call createFiber method to create an instance of FiberNode * @param tag fiberRoot Tags for nodes (LegacyRoot, BatchedRoot, ConcurrentRoot) * @returns {Fiber} */ export function createHostRootFiber(tag: RootTag): Fiber { let mode; // The following code dynamically sets the mode property of rootFiber according to the tag type of fiberRoot // export const NoMode = 0b0000; => 0 // export const StrictMode = 0b0001; => 1 // export const BatchedMode = 0b0010; => 2 // export const ConcurrentMode = 0b0100; => 4 // export const ProfileMode = 0b1000; => 8 if (tag === ConcurrentRoot) { mode = ConcurrentMode | BatchedMode | StrictMode; } else if (tag === BatchedRoot) { mode = BatchedMode | StrictMode; } else { mode = NoMode; } ... // Call createFiber method to create and return an instance of FiberNode // HostRoot represents the root node of the fiber tree // Other tag types can be found in the shared/ReactWorkTags.js file return createFiber(HostRoot, null, null, mode); } /** * Create and return an instance of FiberNode * @param tag The type used to tag the fiber node (all types are stored in the shared/ReactWorkTags.js file) * @param pendingProps Represents the props data to be processed * @param key It is used to uniquely identify a fiber node (especially in some list data structures, it is generally required to add additional key attribute for each DOM node or component, which will be used in the subsequent reconciliation stage) * @param mode Represents the pattern of a fiber node * @returns {FiberNode} */ const createFiber = function( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ): Fiber { // $FlowFixMe: the shapes are exact here but Flow doesn't like constructors // The FiberNode constructor is used to create an instance of a FiberNode, that is, a fiber node return new FiberNode(tag, pendingProps, key, mode); };
So far, we have successfully created a fiber node. As mentioned above, similar to the DOM tree structure, the fiber node will also form a fiber tree corresponding to the DOM tree structure, which is based on a single chain table. The fiber node we just created above can be used as the root node of the entire fiber tree, that is, the RootFiber node. At this stage, we do not need to care about all the attributes contained in a fiber node, but we can pay a little attention to the following related attributes:
/** * FiberNode Constructor * @param tag Type used to tag a fiber node * @param pendingProps Represents the props data to be processed * @param key It is used to uniquely identify a fiber node (especially in some list data structures, it is generally required to add additional key attribute for each DOM node or component, which will be used in the subsequent reconciliation stage) * @param mode Represents the pattern of a fiber node * @constructor */ function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { // Instance // Type used to tag a fiber node this.tag = tag; // Used to uniquely identify a fiber node this.key = key; ... // For a rootFiber node, the stateNode attribute points to the corresponding fiberRoot node // For a child fiber node, the stateNode attribute points to the corresponding component instance this.stateNode = null; // Fiber // The following properties create a single chain table tree structure // return property always points to the parent node // The child attribute always points to the first child node // The sibling attribute always points to the first sibling this.return = null; this.child = null; this.sibling = null; // The index property indicates the index of the current fiber node this.index = 0; ... // Represents the props data to be processed this.pendingProps = pendingProps; // Indicates the props data previously stored this.memoizedProps = null; // Represents the update queue // For example, in common setState operations // In fact, the data to be updated will be stored in the updateQueue queue here for subsequent scheduling this.updateQueue = null; // Indicates previously stored state data this.memoizedState = null; ... // Represents the pattern of a fiber node this.mode = mode; // Indicates the expiration time of the current update task, that is, the update task will be completed after that time this.expirationTime = NoWork; // Indicates the expiration time of the task with the highest priority in the subfiber node of the current fiber node // The value of this attribute will be dynamically adjusted according to the task priority in the sub fiber node this.childExpirationTime = NoWork; // Used to point to another fiber node // The two fiber nodes use the alternate attribute to refer to each other, forming a double buffer // The fiber node pointed to by the alternate attribute is also known as the workInProgress node in task scheduling this.alternate = null; ... }
Other useful properties the author has written in the source code related notes, interested friends can Github View the complete comment information on to help understand. Of course, at this stage, some of these attributes are still difficult to understand, but it doesn't matter. They will be broken one by one in the following content and series. In this section, we mainly want to understand the confusing concepts of FiberRoot and RootFiber and the relationship between them. At the same time, we need to pay special attention to the fact that multiple fiber nodes can form a tree structure based on a single chain table. Through their own return, the child and sibling attributes can establish relationships among multiple fiber nodes. To make it easier to understand the relationship between multiple fiber nodes and their attributes, let's review Last article For the simple example in, in the src/App.js file, we modify the default root component App generated by the create react App scaffold to the following form:
import React, {Component} from 'react'; function List({data}) { return ( <ul className="data-list"> { data.map(item => { return <li className="data-item" key={item}>{item}</li> }) } </ul> ); } export default class App extends Component { constructor(props) { super(props); this.state = { data: [1, 2, 3] }; } render() { return ( <div className="container"> <h1 className="title">React learning</h1> <List data={this.state.data} /> </div> ); } }
The resulting DOM structure is as follows:
<div class="container"> <h1 class="title">React learning</h1> <ul class="data-list"> <li class="data-item">1</li> <li class="data-item">2</li> <li class="data-item">3</li> </ul> </div>
Based on the DOM structure and the above analysis process of source code, we can try to draw a diagram to deepen our impression:
summary
This paper mainly analyzes the implementation principle of ReactDOM.render method line by line from scratch based on the content of the previous article. The implementation process and call stack behind it are very complex, and they are also in the process of continuous exploration. In this paper, we mainly introduce two core concepts: FiberRoot and RootFiber. Only by understanding and distinguishing these two concepts can we better understand the Fiber architecture of React and the execution process of tasks in the task scheduling phase. The process of reading the source code is painful, but at the same time, the benefits are huge. In order to avoid the article being too boring, we plan to separate the source code content into a series of articles to interpret separately. The interval between the two periods can be used to review the previous content and avoid stuttering a fat man, but the effect is not good.
Thank you for reading.
If you feel that the content of this article is helpful to you, can you do a little bit of help, pay close attention to the public address [front end] of the pen, try to create some front-end technology dry goods every week, pay attention to the public number and invite you to join the front end technology exchange group. We can work hand in hand with each other and make progress together.
Article synchronized to Github blog , if the article is OK, welcome to star!
Your praise is worth more efforts!
Growing up in adversity, only by continuous learning, can we become better ourselves and share with you!