Now (2018) React is becoming more and more 🔥 in front-end development. I use React a lot in projects myself, but I am always curious about the underlying implementation principle of React. I tried to read the React source code several times, but it was really difficult. Not long ago, I read several articles on how to implement React by myself. Based on these information and my own ideas, I will implement a simple version of React with only 200 lines of code starting from 0. After reading this article, I believe you will have a better understanding of the internal implementation principle of React. However, we need to understand some important concepts related to React, such as the difference between component (class) and component instance, diff algorithm, and life cycle, before implementing react.
1 Basic concepts: Component, Instance, Element, JSX, DOM
First of all, we need to understand some confusing concepts. When I first learned React, I was a little confused about the difference between them. A few days ago, when I was discussing with a new student, I found that he could not distinguish component from component instance, so it was necessary to understand the difference between these concepts and association. We’ll implement this simplified react later in this article based on these concepts.
Component (Component)
Component is a Component that we usually implement. It can be a class Component or a functional Component. React.component.component.react.component.react.component.react.component.react.component.react.component.react.component.react.component.react.component.react.component.react.component.react.component.react.component.react.component There is no expansion here. Functional components are used to simplify the implementation of simple components by writing a function with props as the component property and react Element as the render method of the class component. Here are three ways to implement the Welcome component:
// Component
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>; }}Copy the code
// PureComponent
class Welcome extends React.PureComponent {
render() {
return <h1>Hello, {this.props.name}</h1>; }}Copy the code
// functional component
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
Copy the code
Instance (component instance)
People familiar with object-oriented programming know the relationship between classes and instances. In React, a component instance is the result of a component class being instantiated. React does not require us to instantiate a component instance by ourselves. This process is actually done internally by React, so we do not have many opportunities to actually interact with component instances. We are more exposed to Element, as we usually write JSX as a representation of Element (more on that later). Component instances are not used much, but they are occasionally used, which is ref. Ref can refer to a DOM node or an instance of a class component, but not to functional components, which cannot be instantiated. Refs can refer to a component instance. Refs and the DOM can refer to a component instance.
element
Element was mentioned earlier, both the render method of the class component and the return value of the functional component. So what exactly is element here? Actually very simple, is a pure object (plain object), and the pure object contains two attributes: type: (string | ReactClass) and props: object, pay attention to the element is not a component instance, but a pure object. Although an element is not a component instance, it is related to the component instance. An element is a description of the component instance or DOM node. If type is string, it represents a DOM node, and if type is function or class, it represents a component instance. For example, the following two elements represent a DOM node and a component instance:
// Describe the DOM node
{
type: 'button'.props: {
className: 'button button-blue'.children: {
type: 'b'.props: {
children: 'OK! '}}}}Copy the code
function Button(props){
// ...
}
// Describe the component instance
{
type: Button,
props: {
color: 'blue'.children: 'OK! '}}Copy the code
jsx
Once you understand Element, JSX is easy to understand. JSX is simply written in a different way to create Element, considering how much less efficient we would be without it and how much less maintainable the code would be. For example, let’s look at the following JSX example:
const foo = <div id="foo">Hello!</div>;
Copy the code
{id: ‘foo’} {children: Hello! , so it is exactly equivalent to the following representation of pure objects:
{
type: 'div'.props: {
id: 'foo'.children: 'Hello! '}}Copy the code
So how does React convert the JSX syntax to pure objects? Pragma pragma pragma pragma pragma pragma Pragma Pragma Pragma Pragma For example, if we set the compile indicator to point to the createElement function: /** @jsx createElement */, the previous JSX code will compile to:
var foo = createElement('div', {id:"foo"}, 'Hello! ');
Copy the code
As you can see, JSX compilation is a transition from <, > markup to function call writing. React creates an element by simply implementing the createElement function. React creates an element by simply implementing the createElement function.
function createElement(type, props, ... children) {
props = Object.assign({}, props); props.children = [].concat(... children) .filter(child= >child ! =null&& child ! = =false)
.map(child= > child instanceof Object ? child : createTextElement(child));
return {type, props};
}
Copy the code
dom
Dom is also a brief introduction, and as a front-end developer, you should be familiar with the concept. We can create a DOM node div like this:
const divDomNode = window.document.createElement('div');
Copy the code
All DOM nodes are instances of the HTMLElement class.
window.document.createElement('div') instanceof window.HTMLElement;
/ / output true
Copy the code
The HTMLElementAPI can be found here: HTMLElement Introduction. Therefore, the DOM node is an instance of the HTMLElement class; Similarly, in React, a component instance is an instance of a component class. Element describes a component instance and a DOM node. Having introduced these basic concepts, let’s draw a diagram to illustrate the relationship between these concepts:
2. Virtual DOM and DIff algorithm
Those of you who have used React are familiar with these two concepts: the virtual DOM and the Diff algorithm. An element is a pure object description of a DOM node or component instance. It is not a real DOM node, so it is a virtual DOM. React provides declarative component writing that automatically updates components when their props or state changes. The entire page actually corresponds to a DOM node tree, and each change in the component props or state is reflected first in the virtual DOM tree and then in the rendering of the page DOM node tree.
So what does the virtual DOM have to do with diff? The reason for the diff algorithm is to improve the rendering efficiency. Imagine that the efficiency would be very low if all relevant DOM nodes were deleted and rebuilt after each change of the component’s state or props. Therefore, there are two virtual DOM trees inside React, representing the current state and the next state respectively. The setState call triggers the execution of the DIff algorithm, and a good DIff algorithm must reuse the existing DOM nodes as much as possible to avoid the overhead of re-creation. I use the following figure to show the relationship between virtual DOM and diff algorithm:
react
1 frame
Pointer to the current
setState
Frame 2,
Next pointer
diff
Frame 2,
1 frame
dom
Again, I want to emphasize how to generate the virtual DOM after setState, because this is important and easy to overlook. You’ve already seen what the virtual DOM is. It’s just an Element tree. So where did the Element tree come from? The render method returns the following flow chart to give you an impression:
react
The diff algorithm
react
Reconciliation
The diff algorithm
reconcile
To heal,
check
react
Virtual dom
The diff algorithm
reconcile
diff
3 life cycle and DIff algorithm
What does the life cycle have to do with diff? Here we use componentDidMount, componentWillUnmount, ComponentWillUpdate and componentDidUpdate as an example to illustrate the relationship between the two. As we know, the setState call will be followed by the render call to generate a new virtual DOM tree, and this virtual DOM tree may differ from the previous frame as follows:
- A component has been added;
- A component was deleted.
- Some properties of a component are updated.
Therefore, our implementation of the DIff algorithm calls these lifecycle functions at the corresponding time nodes.
It’s important to note that in frame 1, we know that each react entry is:
ReactDOM.render(
<h1>Hello, world!</h1>.document.getElementById('root'));Copy the code
ReactDom. Render also generates a virtual DOM tree, but this tree is the first frame to be generated and no previous frame is used for diff, so all components of this virtual DOM tree will only call the mount life cycle function. ComponentDidMount, componentWillUnmount.
4 implement
With these concepts in mind, it’s not hard to implement a simple react version. It’s important to note that this section is partially based on this blog’s implementation Didact: A DIY Guide to Build Your Own React. Now let’s take a look at what apis we want to implement, and we’ll end up using them as follows:
// Declare compilation instructions
/** @jsx DiyReact.createElement */
// Import the API we will implement next
const DiyReact = importFromBelow();
// Business code
const randomLikes = (a)= > Math.ceil(Math.random() * 100);
const stories = [
{name: "React".url: "https://reactjs.org/".likes: randomLikes()},
{name: "Node".url: "https://nodejs.org/en/".likes: randomLikes()},
{name: "Webpack".url: "https://webpack.js.org/".likes: randomLikes()}
];
const ItemRender = props= > {
const {name, url} = props;
return (
<a href={url}>{name}</a>
);
};
class App extends DiyReact.Component {
render() {
return( <div> <h1>DiyReact Stories</h1> <ul> {this.props.stories.map(story => { return <Story name={story.name} url={story.url} />; })} </ul> </div> ); } componentWillMount() { console.log('execute componentWillMount'); } componentDidMount() { console.log('execute componentDidMount'); } componentWillUnmount() { console.log('execute componentWillUnmount'); } } class Story extends DiyReact.Component { constructor(props) { super(props); this.state = {likes: Math.ceil(Math.random() * 100)}; } like() { this.setState({ likes: this.state.likes + 1 }); } render() { const {name, url} = this.props; const {likes} = this.state; const likesElement = <span />; Return (< li > < button onClick = {e = > this. Like ()} > {likes} < b > ❤ ️ < / b > < / button > < ItemRender {... itemRenderProps} /> </li> ); } // shouldcomponentUpdate() { // return true; // } componentWillUpdate() { console.log('execute componentWillUpdate'); } componentDidUpdate() { console.log('execute componentDidUpdate'); } // Render the component to the root dom node DiyReact. Render (<App stories={stories} />, document.getelementbyid ("root"));Copy the code
We use the Render, createElement, and Component apis in this business code, so the next task is to implement these apis and wrap them in a function importFromBelow.
4.1 realize the createElement method
CreateElement (); createElement (); createElement (); createElement (); createElement ();
function createElement(type, props, ... children) {
props = Object.assign({}, props); props.children = [].concat(... children) .filter(child= >child ! =null&& child ! = =false)
.map(child= > child instanceof Object ? child : createTextElement(child));
return {type, props};
}
Copy the code
4.2 implementation render
Note that this render is equivalent to reactdom. render, not the Component’s render method, which is later in the Component implementation.
// rootInstance is used to cache a frame of virtual DOM
let rootInstance = null;
function render(element, parentDom) {
// prevInstance points to the previous frame
const prevInstance = rootInstance;
The element argument points to the newly generated virtual DOM tree
const nextInstance = reconcile(parentDom, prevInstance, element);
// After the Reconcile algorithm (diff) is called, rooInstance points to the latest frame
rootInstance = nextInstance;
}
Copy the code
The Render function is as simple as reconciling two frames of the virtual DOM and then reconciling the rootInstance to the new virtual DOM. If we are careful, we will notice that the new virtual DOM is element (as in Reconcile) and the virtual DOM is instance (not a component instance). In short, the Render method simply calls the Reconcile method to compare two virtual DOM frames.
4.3 implementation instantiate
So what’s the difference between instance and Element? In fact, instance means simply wrapping element in a new layer and including the corresponding DOM in the package. This is not difficult to understand, because when we call the reconcile diff comparison, we apply the new and the new to the real DOM, so we need to relate them to the DOM. The instantiate function implemented below does just that. Note that since Element includes both dom and Component types (as determined by the type field, refer back to element in the first section), it needs to be handled differently:
The element. Type of the DOM type is string, and the corresponding instance structure is {element, dom, childInstances}.
Component element.type is ReactClass and the corresponding instance is {dom, Element, childInstance, publicInstance}. Notice that publicInstance is the component instance described earlier.
function instantiate(element) {
const {type, props = {}} = element;
const isDomElement = typeof type === 'string';
if (isDomElement) {
/ / create the dom
const isTextElement = type === TEXT_ELEMENT;
const dom = isTextElement ? document.createTextNode(' ') : document.createElement(type);
// Set the event and data properties of the DOM
updateDomProperties(dom, [], element.props);
const children = props.children || [];
const childInstances = children.map(instantiate);
const childDoms = childInstances.map(childInstance= > childInstance.dom);
childDoms.forEach(childDom= > dom.appendChild(childDom));
const instance = {element, dom, childInstances};
return instance;
} else {
const instance = {};
const publicInstance = createPublicInstance(element, instance);
const childElement = publicInstance.render();
const childInstance = instantiate(childElement);
Object.assign(instance, {dom: childInstance.dom, element, childInstance, publicInstance});
returninstance; }}Copy the code
Note that since both DOM nodes and component instances may have child nodes, there is logic for recursively instantiating in the instantiate function.
4.4 Category components and functional components
As we mentioned earlier, components include class components and functional components. I often use these two types of components in my business. If a component is only used for rendering, I usually use functional components because the code logic is simple and easy to understand. How does React internally differentiate between the two components? The problem is as simple as it is complicated. The reason for this is that React’s internal implementation is relatively simple, but this simple implementation is determined after various considerations. React tells a Class from a Function. How Does React Tell a Class from a Function? . The simple answer is that we need to implement a class component that inherits from react.ponent, so we need to mark react.ponent first and then determine if that tag is present in the prototype chain of element.type when instantiating the component.
/ / to play tag
Component.prototype.isReactComponent = {};
// Distinguish component types
const type = element.type;
const isDomElement = typeof type === 'string';
constisClassElement = !! (type.prototype && type.prototype.isReactComponent);Copy the code
Here we update the previous instantiate function to distinguish functional components from class components:
function instantiate(element) {
const {type, props = {}} = element;
const isDomElement = typeof type === 'string';
constisClassElement = !! (type.prototype && type.prototype.isReactComponent);if (isDomElement) {
/ / create the dom
const isTextElement = type === TEXT_ELEMENT;
const dom = isTextElement ? document.createTextNode(' ') : document.createElement(type);
// Set the event and data properties of the DOM
updateDomProperties(dom, [], element.props);
const children = props.children || [];
const childInstances = children.map(instantiate);
const childDoms = childInstances.map(childInstance= > childInstance.dom);
childDoms.forEach(childDom= > dom.appendChild(childDom));
const instance = {element, dom, childInstances};
return instance;
} else if (isClassElement) {
const instance = {};
const publicInstance = createPublicInstance(element, instance);
const childElement = publicInstance.render();
const childInstance = instantiate(childElement);
Object.assign(instance, {dom: childInstance.dom, element, childInstance, publicInstance});
return instance;
} else {
const childElement = type(element.props);
const childInstance = instantiate(childElement);
const instance = {
dom: childInstance.dom,
element,
childInstance,
fn: type
};
returninstance; }}Copy the code
As you can see, if it is a functional component, instead of instantiating the component, we call the function directly to get the virtual DOM.
4.5 Implement reconcile(Diff Algorithm)
As Reconcile is the core of React, it is obviously very important to quickly render newly set states. Therefore, React will try to use existing nodes as much as possible, instead of dynamically creating all relevant nodes each time. However, react is more powerful than that. React16 upgrades the Reconcile algorithm from the previous STACK architecture to fiber architecture, which is a further step in performance optimization. The content related to fiber will be introduced in the next section. In order to be simple and easy to understand, we still use the algorithm similar to stack architecture to realize it. For fiber, we only need to know its scheduling principle, of course, we can realize another version based on Fiber architecture later.
First, take a look at the entire reconcile algorithm flow:
- If it is new
instance
, then you need to instantiate oneinstance
andappendChild
; - If it is not new
instance
, but deleteinstance
, then the needremoveChild
; - If neither add nor delete
instance
, then need to seeinstance
thetype
Does it change? If it does change, then the node can’t be reused and needs to be instantiatedinstance
And thenreplaceChild
; - if
type
Existing nodes can be reused with no changes, in which case it is considered nativedom
The node is still our custom implementationreact
Node, which is handled differently in both cases.
After understanding the big process, we just need to execute the life cycle function at the right point in time. Here is the implementation:
function reconcile(parentDom, instance, element) {
if (instance === null) {
const newInstance = instantiate(element);
// componentWillMount
newInstance.publicInstance
&& newInstance.publicInstance.componentWillMount
&& newInstance.publicInstance.componentWillMount();
parentDom.appendChild(newInstance.dom);
// componentDidMount
newInstance.publicInstance
&& newInstance.publicInstance.componentDidMount
&& newInstance.publicInstance.componentDidMount();
return newInstance;
} else if (element === null) {
// componentWillUnmount
instance.publicInstance
&& instance.publicInstance.componentWillUnmount
&& instance.publicInstance.componentWillUnmount();
parentDom.removeChild(instance.dom);
return null;
} else if(instance.element.type ! == element.type) {const newInstance = instantiate(element);
// componentDidMount
newInstance.publicInstance
&& newInstance.publicInstance.componentDidMount
&& newInstance.publicInstance.componentDidMount();
parentDom.replaceChild(newInstance.dom, instance.dom);
return newInstance;
} else if (typeof element.type === 'string') {
updateDomProperties(instance.dom, instance.element.props, element.props);
instance.childInstances = reconcileChildren(instance, element);
instance.element = element;
return instance;
} else {
if (instance.publicInstance
&& instance.publicInstance.shouldcomponentUpdate) {
if(! instance.publicInstance.shouldcomponentUpdate()) {return; }}// componentWillUpdateinstance.publicInstance && instance.publicInstance.componentWillUpdate && instance.publicInstance.componentWillUpdate(); instance.publicInstance.props = element.props;let newChildElement;
if (instance.publicInstance) { / / class components
instance.publicInstance.props = element.props;
newChildElement = instance.publicInstance.render();
} else { // Functional components
newChildElement = instance.fn(element.props);
}
const oldChildInstance = instance.childInstance;
const newChildInstance = reconcile(parentDom, oldChildInstance, newChildElement);
// componentDidUpdate
instance.publicInstance
&& instance.publicInstance.componentDidUpdate
&& instance.publicInstance.componentDidUpdate();
instance.dom = newChildInstance.dom;
instance.childInstance = newChildInstance;
instance.element = element;
returninstance; }}function reconcileChildren(instance, element) {
const {dom, childInstances} = instance;
const newChildElements = element.props.children || [];
const count = Math.max(childInstances.length, newChildElements.length);
const newChildInstances = [];
for (let i = 0; i < count; i++) {
newChildInstances[i] = reconcile(dom, childInstances[i], newChildElements[i]);
}
return newChildInstances.filter(instance= >instance ! = =null);
}
Copy the code
If you read the Reconcile algorithm, one of you might wonder why it’s called the stack algorithm, and here’s a quick explanation. As you can see from the previous implementation, each state update of a component triggers the reconcile execution, which is also a recursive process and does not stop until all the nodes are recursively executed, hence the stack algorithm. Since it is a recursive process, the diff algorithm must be executed once it starts, which may block the thread. Moreover, since JS is single-threaded, it may affect user input or UI rendering frame rate and reduce user experience. However, this issue was resolved with the update to Fiber architecture in Act16.
4.6 Overall Code
Combine all of this code to create a complete react version with less than 200 lines of code. See DiyReact for the complete code.
5 fiber architecture
React16 has upgraded the reconcile algorithm architecture from Stack to Fiber architecture. We have already mentioned the disadvantage of stack architecture, that is, it uses recursions. Once started, it cannot be paused, only to be completed in one go. It will degrade the user experience.
The Fiber architecture is different. The underlying layer is the requestIdleCallback, which is used to schedule the execution of the DIff algorithm. For an introduction to requestIdleCallback, see my previous article on JAVASCRIPT event loops (browser-side, Node-side). RequestIdlecallback features, as the name suggests, the use of free time to complete tasks. Note that the free time is relative to higher priority tasks such as user input and UI rendering.
Here is a brief introduction to the origin of the name Fiber, because I was curious about why it was called fiber in the first place. Fiber (Computer Science) is not a new word. React can suspend the diff algorithm at any time and then execute it when there is free time. This is a more detailed scheduling algorithm, hence it is called fiber architecture. In this paper, I will briefly introduce fiber, and then I will summarize a separate article later.
6 Reference Materials
Mainly refer to the following materials:
- React Components, Elements, and Instances
- Refs and the DOM
- HTMLElement introduction
- Didact: a DIY guide to build your own React
- How Does React Tell a Class from a Function?
- Lin Clark – A Cartoon Intro to Fiber – React Conf 2017
- Let’s fall in love with React Fiber