📜 ⬆️ ⬇️

Under the hood of React. We write our implementation from scratch

In this series of articles, we will create our own React implementation from scratch. At the end you will have an understanding of how React works, what methods of the component life cycle it calls and for what. The article is intended for those who have already used React and want to learn about its device, or very curious.

image

This article is a translation of React Internals, Part One: basic rendering

This is actually the first of five articles.


  1. Basics of rendering <- we are here
  2. ComponentWillMount and componentDidMount
  3. Update
  4. setState
  5. Transactions

The material was created when React 15.3 was relevant, in particular the use of ReactDOM and stack reconciler. React 16 and above has some changes. However, this material remains relevant, as it gives a general idea of ​​what is happening “under the hood”.

Part 1. Basics of rendering


Elements and Components


There are three types of entities in React: the native DOM element, the virtual React element, and the component.
')

Native DOM elements


These are the DOM elements that the browser uses to create a web page, for example, div, span, h1. React creates them by calling document.createElement (), and interacts with the page using methods of the browser-based DOM API, such as element.insertBefore (), element.nodeValue, and others.

Virtual React Element


A virtual React element (often simply called an “element”) is a javascript object that contains the necessary properties in order to create or update a native DOM element or tree of such elements. Based on the virtual React element, native DOM elements are created, such as div, span, h1, and others. We can say that the virtual React element is an instance of a user-defined composite component (user defined composite component), more on this below.

Component


Component is a fairly generic term in React. Components are entities with which React makes various manipulations. Different components serve different purposes. For example, ReactDomComponent from the ReactDom library is responsible for the binding between React elements and their corresponding native DOM elements.

Custom composite components


Most likely you have already come across this type of components. When you call React.createClass () or use the ES6 classes via extend React.Component, you create a custom composite component. Such a component has lifecycle methods, such as componentWillMount, shouldComponentUpdate, and others. We can redefine them to add some logic. In addition, other methods are being created, such as mountComponent, receiveComponent. These methods are used only by React for its internal purposes; we do not interact with them at all.

ZanudaMode = on
In fact, user-created components are initially incomplete. React wraps them in ReactCompositeComponentWrapper, which adds all lifecycle methods to our components, after which React can control them (insert, update, etc.).

React declarative


When it comes to custom components, our task is to define the classes of these components, but we do not create instances of these classes. They are created by React, when necessary.

We also do not create elements explicitly using the imperative style, instead we write in a declarative style using JSX:

class MyComponent extends React.Component { render() { return <div>hello</div>; } } 

This code with JSX markup is translated by the compiler into the following:

 class MyComponent extends React.Component { render() { return React.createElement('div', null, 'hello'); } } 

That is, in essence, it turns into an imperative element creation construct through an explicit call to React.createElement (). But this construct is inside the render () method, which we obviously do not call; React itself will call this method when needed. Therefore, to perceive React is exactly as declarative: we describe what we want to get, and React determines how to do it.

Write your little React


Having obtained the necessary technical basis, we will begin to create our own implementation of React. This will be a very simplified version, let's call it Feact.

Suppose we want to create a simple Feact application whose code would look like this:

 Feact.render(<h1>hello world</h1>, document.getElementById('root')); 

To begin, make a digression about JSX. This is precisely a “retreat”, because JSX parsing is a separate big topic that we will omit as part of our implementation of Feact. If we were dealing with processed JSX, we would see the following code:

 Feact.render( Feact.createElement('h1', null, 'hello world'), document.getElementById('root') ); 

That is, we use Feact.createElement instead of JSX. So we implement this method:

 const Feact = { createElement(type, props, children) { const element = { type, props: props || {} }; if (children) { element.props.children = children; } return element; } }; 

The returned item is a simple object representing what we want to render.

What does Feact.render () do?


Calling Feact.render (), we pass two parameters: what we want to render and where. This is the starting point of any React application. Write the implementation of the render () method for Feact:

 const Feact = { createElement() { /*   */ }, render(element, container) { const componentInstance = new FeactDOMComponent(element); return componentInstance.mountComponent(container); } }; 

Upon completion of render (), we get a ready-made web page. The creation of DOM elements is done by the FeactDOMComponent. Let's write its implementation:

 class FeactDOMComponent { constructor(element) { this._currentElement = element; } mountComponent(container) { const domElement = document.createElement(this._currentElement.type); const text = this._currentElement.props.children; const textNode = document.createTextNode(text); domElement.appendChild(textNode); container.appendChild(domElement); this._hostNode = domElement; return domElement; } } 

The mountComponent method creates a DOM element and stores it in this._hostNode. We will not use it now, but it will return to this in the following sections.

The current version of the application can be viewed in fiddle .

Literally 40 lines of code were enough to make the most primitive implementation of React. The Feact created by us is unlikely to conquer the world, but it reflects well the essence of what is happening under the hood of React.

Add custom components


Our Feact should be able to render not only the elements available in HTML (div, span, etc.), but also the user defined composite component:
The previously described method Feact.createElement () currently suits us, therefore I will not repeat it in the code listing
 const Feact = { createClass(spec) { function Constructor(props) { this.props = props; } Constructor.prototype.render = spec.render; return Constructor; }, render(element, container) { //      //   , //    } }; const MyTitle = Feact.createClass({ render() { return Feact.createElement('h1', null, this.props.message); } }; Feact.render({ Feact.createElement(MyTitle, { message: 'hey there Feact' }), document.getElementById('root') ); 

Remember, if JSX were available, a call to the render () method would look like this:

 Feact.render( <MyTitle message="hey there Feact" />, document.getElementById('root') ); 

We passed the custom component class to createElement. A virtual React element can represent either a regular DOM element or a custom component. We distinguish them as follows: if we pass a string type, then this is a DOM element; if a function, then this element represents a custom component.

Improve Feact.render ()


If you look closely at the current code, you will see that Feact.render () cannot handle custom components. Fix this:

 Feact = { render(element, container) { const componentInstance = new FeactCompositeComponentWrapper(element); return componentInstance.mountComponent(container); } } class FeactCompositeComponentWrapper { constructor(element) { this._currentElement = element; } mountComponent(container) { const Component = this._currentElement.type; const componentInstance = new Component(this._currentElement.props); const element = componentInstance.render(); const domComponentInstance = new FeactDOMComponent(element); return domComponentInstance.mountComponent(container); } } 

We created a wrapper for the item to be transferred. Inside the wrapper, we create an instance of the custom component class and call its componentInstance.render () method. The result of this method can be passed to the FeactDOMComponent, where the corresponding DOM elements will be created.

Now we can create and render custom components. Feact will create DOM nodes based on custom components, and change them depending on the properties (props) of our custom components. This is a significant improvement to our feact.
Note that FeactCompositeComponentWrapper directly creates a FeactDOMComponent. Such a close relationship is bad. We will fix this later. If the same close relationship existed in React, then only web applications could be created. Adding an additional layer ReactCompositeComponentWrapper allows you to separate the React logic for managing virtual elements and the final display of native elements, which allows using React not only when creating web applications, but also, for example, React Native for mobile.

Improving custom components


Created custom components can only return native DOM elements, if you try to return other custom components, we get an error. Correct this flaw. Imagine that we would like to execute the following code without errors:

 const MyMessage = Feact.createClass({ render() { if (this.props.asTitle) { return Feact.createElement(MyTitle, { message: this.props.message }); } else { return Feact.createElement('p', null, this.props.message); } } } 

The render () method of a custom component can return either a native DOM element or another custom component. If the asTitle property is true, then FeactCompositeComponentWrapper will return a custom component for FeactDOMComponent where an error will occur. Fix the FeactCompositeComponentWrapper:

 class FeactCompositeComponentWrapper { constructor(element) { this._currentElement = element; } mountComponent(container) { const Component = this._currentElement.type; const componentInstance = new Component(this._currentElement.props); let element = componentInstance.render(); while (typeof element.type === 'function') { element = (new element.type(element.props)).render(); } const domComponentInstance = new FeactDOMComponent(element); domComponentInstance.mountComponent(container); } } 

In truth, we now made a crutch to meet current needs. Calling the render method will return the child components until the native DOM element is returned. This is bad because such child components will not participate in the life cycle. For example, in this case we will not be able to implement the call componentWillMount. We will fix this later.

And again we fix Feact.render ()


The first version of Feact.render () could only process native DOM elements. Now only custom components without native support are processed correctly. It is necessary to handle both cases. You can write a factory that will create a component depending on the type of item transferred, but in React another way was chosen: just wrap any incoming component into another component:

 const TopLevelWrapper = function(props) { this.props = props; }; TopLevelWrapper.prototype.render = function() { return this.props; }; const Feact = { render(element, container) { const wrapperElement = this.createElement(TopLevelWrapper, element); const componentInstance = new FeactCompositeComponentWrapper(wrapperElement); //   } }; 

TopLevelWrapper is essentially a custom component. It can also be defined by calling Feact.createClass (). His render method simply returns the element passed to it. Now each element is wrapped around TopLevelWrapper, and the FeactCompositeComponentWrapper will always receive a custom component as input.

Conclusion of the first part


We implemented Feact, which can render components. The generated code shows basic rendering concepts. The actual rendering in React is much more complicated, and it covers events, focus, window scrolling, performance, etc.

The final jsfiddle of the first part.

Source: https://habr.com/ru/post/458916/


All Articles