preface
As a beginner of React, I watched the video of Implementing React from zero of Mount Everest architecture during the holiday, and then clicked it. In order to deepen my memory and understanding, I wrote this blog.
The code in this article is from the video, what I did is to comb through the whole logic according to my own understanding, so this article is not strictly a blog, it should be a study note.
If there is an incorrect place welcome comment pointed out!
One of the simplest implementations
call
// index.js
import React from './react';
React.render('hello world'.document.getElementById('root'));
Copy the code
implementation
The result of the above two lines of code is a hello World string in a container with an ID of root, so we can simply define the Render method
// react.js
function render(el, container) {
$(container).html(el); // jQuery API
}
export default React {
render,
}
Copy the code
Function components and class components
However, as we will soon discover, we don’t always define a component in such a simple way. We use react to define components in two ways: functions and classes.
How do these two types of EL stuff into containers? We can take a brief look at their calls and Babel translation results.
Function component
function App() {
return (
<div style="color: red" onClick={function() {aler(1)}} >
Hello
<span>world</span>
</div>
)
}
React.render(App(), document.getElementById('root'));
Copy the code
After bebel’s translation, it was found that he called the React ceateElement method. If you are familiar with the virtual DOM, you may be familiar with it.
React.render(
React.createElement(
'div',
{
style: "color: red".onClick: function () { alert(1); }},'hello',
React.createElement('span', {}, 'world')),document.getElementById('root'));Copy the code
Class components
class App extends React.Component {
render () {
return (
<div style="color: red" onClick={function() {aler(1)}} >
Hello
<span>world</span>
</div>
)
}
}
React.render(App, document.getElementById('root'));
Copy the code
After a Bebel translation, we see that it is very similar to a function component, but the first argument is changed from a string to an App class (function) :
React.render(
React.createElement(App),
document.getElementById('root'));Copy the code
React.createElement
We see that both calls, after being migrated, call the react. createElement method.
The JSX syntax is
// If the object is a virtual DOM, el instanceof DOM
class Element {
constructor(type, props) {
this.type = type;
this.props = props; }}Virtual DOM / * * * returns * @ param {string | function} type node type * @ param {object} props node properties * @ param * / {array} children child node
export default function createElement(type, props = {}, ... children) {
props.children = children; // array
return new Element(type, props);
}
Copy the code
The react. createElement returns an Element class. The nice thing about structuring it is that we can use instanceof to determine if the object is a virtual DOM.
Virtual DOM
The virtual DOM is basically an object representing a DOM node.
For example:
We can describe it as a node with a tag of type div, attribute id, attribute value app, and a child node of type string.
By extracting the highlighted part, we can describe the node with an object, which is the virtual DOM.
const el = {
type: 'div'.props: {
id: 'app'And the children:'hello']}}Copy the code
What are the benefits of using the virtual DOM to describe nodes?
We know that manipulating real DOM nodes is very expensive, and when data is frequently updated, direct manipulation of real DOM nodes results in frequent redrawing and backflow.
Although a redraw and backflow only takes tens of ms, it may cause a frequent flicker due to redraw, and the user experience is very bad.
React and Vue both have diff algorithms to update the changed DOM nodes by patching them. This improves the update efficiency.
The purpose of this article is to explain why we don’t pass the real DOM node directly into the render method, but take a detour to pass the virtual DOM such a JS object.
Problems with using the virtual DOM
The first problem is that we find that the react. createElement return value is passed in as an EL parameter to the Render method.
function render(el, container) {
$(container).html(el); // jQuery API
}
Copy the code
According to our previous simple implementation, EL should be an HTML string to be rendered into the container.
So the next thing we need to do is convert our virtual DOM object into our HTML string.
The second problem is that since we use the virtual DOM, we need a unique identifier that corresponds to the virtual DOM and the real DOM nodes. To solve this problem, we can add rootId to each node. Of course, in the new version, we use Fiber instead, which is no longer visible.
Render function modification
Now we see that the EL argument of the Render method has three forms of input arguments
- A simple symbol of string or number
- The virtual DOM of the Element class
- Type is a functional component of type string
- Type is a class component of type function
We can simply construct a factory function called createReactUnit that returns an HTML string based on the incoming type.
CreateReactUnit where the return is an instance of the class, through the instance’s getMarkUp method to get the HTML string, in fact, for the extensibility of the code, if the component has other operations, can be implemented through different methods of the class.
// unit.js
class Unit {
constructor(el) {
this.curremtEl = el; }}class ReactTextUnit extends Unit {
// Each different subclass overrides the getMarkUp method to return the HTML string
// rootId is used to distinguish node idsgetMarkUp(rootId) { ... }}class ReactNativeUnit extends Unit { getMarkUp(rootId) { ... }}class ReactComponentUnit extends Unit { getMarkUp(rootId) { ... }}// For different types of el, judge according to the characteristics of the three forms we just entered.
const creatReactUnit = function (el) {
// If it is a simple string or number
if (typeof el === 'string' || typeof el === 'number') {
return new ReactTextUnit(el);
}
// If it is a functional component
if (typeof el === 'object' && typeof el.type === 'string') {
return new ReactNativeUnit(el);
}
// If it is in class form
if (typeof el === 'object' && typeof el.type === 'function') {
return newReactComponentUnit(el); }}// react.js
function render(el, container) {
// Get the corresponding unit instance
let creatReactUnitInstance = creatReactUnit(el);
// Call the instance's getMarkUp method to get the corresponding HTML string
// There is a nextRootIndex parameter, which is rootId
let mark = creatReactUnitInstance.getMarkUp(React.nextRootIndex);
// Stuff the HTML string into the container for rendering
$(container).html(mark);
}
export default const React = {
render,
createElement,
Component,
nextRootIndex: 0,}Copy the code
Write the getMarkUp method
We can now override the getMarkUp method of each subclass to generate the HTML string.
rootId
In order to distinguish different nodes and facilitate the operation of the virtual DOM, we add a data-rootid attribute to each node, as follows:
<div data-rootid="0">
<div data-rootid="0.0">
<span data-rootid="0.0.0">hello</span>
<span data-rootid="0.0.1">world</span>
</div>
</div>
Copy the code
We can easily select the span where the text is hello by using [data-rootid=”0.0.0″].
String | number
Strings can be inserted directly into the container, but in order to distinguish different nodes, we need to cover them with a span and identify them with rootId.
class ReactTextUnit extends Unit {
getMarkUp(rootId) {
this._rootId = rootId;
return `<div data-rootid="${rootId}">
The ${this.curremtEl}
</div>`; }}Copy the code
Functional component
The el class is Element, with properties like type and prop.
el = {
type: "div".props: {style: "color: red".children: [
"app",
{ props: {... },typeƒ}]}}Copy the code
To convert this structure into an HTML string, it’s easy to think of recursion, and the exit of recursion is when the element type of the Children array is primitive.
For example,
can be split into an elementNodediv and a textNodehello, textNode has no children, so the recursion ends, The creatReactUnit method returns an instance of the ReactTextUnit class, and calls the getMarkUp method to get the corresponding HTML string.
class ` extends Unit {
getMarkUp(rootId) {
this._rootId = rootId;
let { type, props } = this.curremtEl;
let children;
// Start tag, the attribute needs to be added backwards by traversing props
let startTag = ` <${type} data-rootid="${rootId}"`
let endTag = ` < /${type}> `;
// props, props ="color: red"
for (let key in props) {
if (key === 'children') {
/** * handles the case of children nodes * recursively traversing the children array, and the join method converts the array to an HTML string */
children = props[key].map((el, index) = > {
// Call createReactUnit recursively to convert the virtual DOM to an HTML string
let childInstance = creatReactUnit(el);
return childInstance.getMarkUp(`${rootId}.${index}`)
}).join(' ');
} else {
// If it is an attribute of a node, type it directly after startTag
startTag += `${key}="${props[key]}"`}}// Returns an HTML string
return `${startTag}>${children}${endTag}`}}Copy the code
Class declarative components
Let reactRendered = Componentinstance.render (); let reactRendered = Componentinstance.render (); This sentence calls).
Once you get the structure, just as with functional components, you recursively call creatReactUnit.
class ReactComponentUnit extends Unit {
getMarkUp(rootId) {
this._rootId = rootId;
let { type: Component, props } = this.curremtEl;
// make sure this. Props is called
let componentInstance = new Component(props);
123
let reactRendered = componentInstance.render();
// Call createReactUnit recursively to render the tag in the component
let reactComponentUnitInstance = creatReactUnit(reactRendered);
returnreactComponentUnitInstance.getMarkUp(rootId); }}Copy the code
Implement the binding of events
So far, we have been able to simply render the component to the page, but when we want to bind events to the component, we find that we cannot bind events to the HTML string.
In this case, we can think of the event delegate mechanism, so how do we know which node to bind the event to? The rootId we define comes in handy.
$(document).on('click'.'[data - rootid = 0.0.0]'.function() {alert(1)}) // The jQuery API is used here, you can also use native
Copy the code
This part of the judgment should be in the ReactNativeUnit class, which is the only class that is involved in the splicing of props, so we add a judgment that, if it matches an attribute starting with ON, binds it as an event.
The complete code is as follows:
class ReactNativeUnit extends Unit {
getMarkUp(rootId) {
this._rootId = rootId;
let { type, props } = this.curremtEl;
console.log(this.curremtEl)
let children;
let startTag = ` <${type} data-rootid="${rootId}"`
let endTag = ` < /${type}> `;
for (let key in props) {
if (key === 'children') {
children = props[key].map((el, index) = > {
let childInstance = creatReactUnit(el);
return childInstance.getMarkUp(`${rootId}.${index}`)
}).join(' ');
} else if (/on[a-z]/i.test(key)) {
/** * handles events * bind events to elements */
let eventType = key.slice(2).toLocaleLowerCase(); // onClick -> click
// Bind events via the event delegate. Parameters are type, subselector, and method to bind
$(document).on(eventType, `[data-rootid=${rootId}] `, props[key])
}
else {
startTag += `${key}="${props[key]}"`}}return `${startTag}>${children}${endTag}`}}Copy the code
Implement a simple life cycle
The react lifecycle for component mounting is componentWillMount and componentDidMount. When components are nested, they are called in the following order:
- Parent will mount
- Children will mount
- Children did mount
- Parent did mount
So how do we make the life cycle appear in this order, we can look at componentWillMount.
componentWillMount
This order is very simple, it is called in recursive order, the farther out the hierarchy, the more first to call.
The only recursive call associated with the component is the getMarkUp method of the ReactComponentUnit class, so just add a line of componentWillMount method to this method. The complete code is as follows.
class ReactComponentUnit extends Unit {
getMarkUp(rootId) {
this._rootId = rootId;
let { type: Component, props } = this.curremtEl;
let componentInstance = new Component(props);
// Call the lifecycle hook function
componentInstance.componentWillMount && componentInstance.componentWillMount();
let reactRendered = componentInstance.render();
let reactComponentUnitInstance = creatReactUnit(reactRendered);
returnreactComponentUnitInstance.getMarkUp(rootId); }}Copy the code
componentDidMount
This hook is called in the reverse order of recursive parsing, mounted first, so it should be called before the return method.
Second, DidMount is a hook that executes after the component has been successfully mounted, which should be executed in the React. Render method, so we trigger a ‘mounted’ event in the Render method as a publisher.
// react.js
function render(el, container) {
let creatReactUnitInstance = creatReactUnit(el);
let mark = creatReactUnitInstance.getMarkUp(React.nextRootIndex);
$(container).html(mark);
// Mount component completion method
$(document).trigger('mounted');
}
Copy the code
$(document).on(‘mounted’,()=> {})
Therefore, the subscription sentence should precede the return.
The complete code is as follows:
class ReactComponentUnit extends Unit {
getMarkUp(rootId) {
this._rootId = rootId;
let { type: Component, props } = this.curremtEl;
let componentInstance = new Component(props);
componentInstance.componentWillMount && componentInstance.componentWillMount();
let reactRendered = componentInstance.render();
let reactComponentUnitInstance = creatReactUnit(reactRendered);
let markUp = reactComponentUnitInstance.getMarkUp(rootId);
// The child starts to mount, subscribing to this method before the parent
$(document).on('mounted', () => {
componentInstance.componentDidMount && componentInstance.componentDidMount();
})
returnmarkUp; }}Copy the code
The complete code
As you can see, the core of the whole logic is to convert JSX syntax into the virtual DOM and recursively traverse the virtual DOM to HTML strings, which makes it easy to understand.
Here is all the code mentioned in this article:
// react.js
import $ from 'jquery';
import creatReactUnit from './unit'
import createElement from './element'
import Component from './component'
let React = {
render,
createElement,
Component,
nextRootIndex: 0,}/** * Render virtual DOM to the page * @param {DOM} el to render the element, JSX syntax * @param {*} container container */
function render(el, container) {
// Get the element in el by the tag
let creatReactUnitInstance = creatReactUnit(el);
let mark = creatReactUnitInstance.getMarkUp(React.nextRootIndex);
$(container).html(mark);
// Mount component completion method
$(document).trigger('mounted');
}
export default React;
Copy the code
// unit.js
import $ from 'jquery';
// Parent class, through which to save parameters
class Unit {
constructor(el) {
this.curremtEl = el; }}class ReactTextUnit extends Unit {
getMarkUp(rootId) {
this._rootId = rootId;
return `<div data-rootid="${rootId}">
The ${this.curremtEl}
</div>`; }}class ReactComponentUnit extends Unit {
getMarkUp(rootId) {
this._rootId = rootId;
let { type: Component, props } = this.curremtEl;
// to facilitate this. Props call
let componentInstance = new Component(props);
componentInstance.componentWillMount && componentInstance.componentWillMount();
// Returned by the component
let reactRendered = componentInstance.render();
123
let reactComponentUnitInstance = creatReactUnit(reactRendered);
let markUp = reactComponentUnitInstance.getMarkUp(rootId);
// The child starts to mount, before the child subscribes to the method
$(document).on('mounted', () => {
componentInstance.componentDidMount && componentInstance.componentDidMount();
})
returnmarkUp; }}class ReactNativeUnit extends Unit {
getMarkUp(rootId) {
this._rootId = rootId;
let { type, props } = this.curremtEl;
console.log(this.curremtEl)
let children;
let startTag = ` <${type} data-rootid="${rootId}"`
let endTag = ` < /${type}> `;
Style ="color: red"
for (let key in props) {
if (key === 'children') {
/** * handles the case where the child node is added recursively, becoming a string */
children = props[key].map((el, index) = > {
let childInstance = creatReactUnit(el);
return childInstance.getMarkUp(`${rootId}.${index}`)
}).join(' ');
} else if (/on[a-z]/i.test(key)) {
/** * handles events * bind events to elements */
let eventType = key.slice(2).toLocaleLowerCase();
// Element, selector, method
$(document).on(eventType, `[data-rootid=${rootId}] `, props[key])
}
else {
startTag += `${key}="${props[key]}"`}}return `${startTag}>${children}${endTag}`}}const creatReactUnit = function (el) {
if (typeof el === 'string' || typeof el === 'number') {
return new ReactTextUnit(el);
}
// 是否是 react 的 虚拟DOM
if (typeof el === 'object' && typeof el.type === 'string') {
return new ReactNativeUnit(el);
}
// If it is in class form
if (typeof el === 'object' && typeof el.type === 'function') {
return newReactComponentUnit(el); }}export default creatReactUnit;
Copy the code
// element.js
class Element {
constructor(type, props) {
this.type = type;
this.props = props; }}Virtual DOM / * * * returns * @ param {string | function} type node type * @ param {object} props node properties * @ param * / {array} children child node
export default function createElement(type, props = {}, ... children) {
props.children = children; // array
return new Element(type, props);
}
Copy the code
// component.js
export default class Component {
constructor(props) {
this.props = props;
}
/** * Update status */setState() { ... }}Copy the code