What is the JSX

JSX looks like HTML, but it’s JavaScript. Babel compiles JSX into the React API before the React code executes.

/ / before compilation
<div className="content">
    <h3>Hello React</h3>
    <p>React is great</p>
</div>
/ / the compiled
React.createElement(
    'div',
    {
        className: 'content'
    },
    React.createElement('h3'.null.'Hello World'),
    React.createElement('p'.null.'React is greate'))Copy the code

React.createElement represents a node element. The first parameter is the name of the node, the second parameter is the attribute of the node, and the following parameters are all child nodes. We can try it ourselves at babeljs.is. React.createElement is used to create the virtual DOM and returns a virtual DOM object. React converts the virtual DOM to the real DOM and displays it on the page.

At runtime, JSX is converted to a React. CreateElement object by React. CreateElement is converted to a virtual DOM object by React, which is converted to a real DOM object by React.

The JSX syntax was created to make it easier for React developers to write user interface code.

What is the virtual DOM

In React, each DOM object has a virtual DOM object, which is a JavaScript representation of the DOM object. In fact, JavaScript objects are used to describe information about the DOM object, such as its type, properties, and child elements.

The virtual DOM object can be thought of as a copy of the DOM object, although the virtual DOM cannot be displayed directly on the screen. The virtual DOM is designed to address performance issues with the React DOM.

/ / before compilation
<div className="content">
    <h3>Hello React</h3>
    <p>React is great</p>
</div>
/ / the compiled
{
    type: "div".props: { className: "content"},
    children: [{type: "h3".props: null.children: [{type: "text".props: {
                        textContent: "Hello React"}}]}, {type: "p".props: null.children: [{type: "text".props: {
                        textContent: "React is greate"}}]}Copy the code

React uses minimal DOM operations to improve the advantages of DOM manipulation. It updates only what needs to be updated. When React first creates DOM objects, it creates virtual DOM objects for each DOM object. React updates all virtual DOM objects before DOM objects are updated. Then, it compares the updated virtual DOM with the updated virtual DOM to find the changed DOM objects. Only the changed DOM is updated to the page to improve THE PERFORMANCE of JS operations on the DOM.

Although the virtual DOM is updated and compared before the operation of the real DOM, the JS operation of its own object is very efficient, and the cost is almost negligible.

Before the React code executes, JSX is converted by Babel to a call to the react. createElement method, which passes in the element type, element attributes, and element children. The createElement method returns the constructed virtual DOM object. Let’s implement a createElement method ourselves.

The createElement method accepts parameters type, props, and childrens. Represents the tag type, tag attribute, and tag child elements. In this method, we return a dummy DOM object in which the type attribute is the value passed in as the parameter, followed by props and children.

function createElement(type, props, ... children) {
    return {
        type,
        props,
        children
    }
}
Copy the code

Here we use TinyReact to analyze the React code. Babel is first configured to compile JSX into Tiny’s createElement method for easy debugging

.babelrc

{
    "presets": [
        "@babel/preset-env"["@babel/preset-react",
            {
                "pragma": "TinyReact.createElement"}}]]Copy the code

Scaffolding warehouse from the address link

src/index.js

import TinyReact from "./TinyReact"

const virtualDOM = (
  <div className="container">
    <h1>Hello, I'm virtual DOM</h1>
  </div>
)

console.log(virtualDOM);
Copy the code

The console prints the result.

{
    "type": "div"."props": {
        "className": "container"
    },
    "children": [{"type":"h1"."props":null."children": [
                "Hello, I'm a virtual DOM."]]}}Copy the code

Here we’re just printing out a simple virtual DOM, but there’s a problem, the text node “hello I’m a virtual DOM” is added directly to the children array as a string, and that’s not right, the correct thing to do is that the text node should also be a virtual DOM object.

We just loop through the children array, say if it’s not an object it’s a text node, we replace it with an object,

function createElement(type, props, ... children) {
    // Iterate over the children object
    constchildElements = [].concat(... children).map(child= > {
        if(child instanceof Object) {
        return child; // the object is returned directly
        } else {
        Call createElement to generate an object instead of an object
        return createElement('text', { textContent: child }); }})return {
    type,
    props,
    children: childElements
    }
}
Copy the code

The text node becomes an object.

{
    "type": "div"."props": {
        "className": "container"
    },
    "children": [{"type":"h1"."props":null."children": [{"type":"text"."props": {
                        "textContent": "Hello, I'm a virtual DOM."
                    },
                    "children": []}]}Copy the code

We all know that a node is not displayed in a component template if it is Boolean or null. We have to deal with it here.

<div className="container">
    <h1>Hello, I'm virtual DOM</h1>
    {
        1= = =2 && <h1>Boolean value node</h1>
    }
</div>
Copy the code
function createElement(type, props, ... children) {
  // Iterate over the children object
  constchildElements = [].concat(... children).reduce((result, child) = > {
    // Determine whether child is Boolean or null
    // Because reduce is used, result is the return value of the previous loop
    if(child ! = =false&& child ! = =true&& child ! = =null) {
      if (child instanceof Object) {
        result.push(child); // the object is returned directly
      } else {
        Call createElement to generate an object instead of an object
        result.push(createElement('text', {
          textContent: child })); }}return result;
  }, [])
  return {
    type,
    props,
    children: childElements
  }
}
Copy the code

We also need to add children to props, just use object. assign to merge props and children.

return {
    type,
    props: Object.assign({ children: childElements}, props),
    children: childElements
}
Copy the code

Convert the virtual DOM to the real DOM

We need to define a render method.

src/tinyReact/render.js

This method takes three parameters, the first is the virtual DOM, the second is the page element to render to, and the third is the old virtual DOM for comparison. The main function of the Render method is to convert the virtual DOM to the real DOM and render it to the page.

import diff from './diff'

function render(virtualDOM, container, oldDOM) {
    diff(virtualDOM, container, oldDOM);
}
Copy the code

This is done once in the diff method, comparing the old virtual DOM if it exists, and placing the current virtual DOM directly in the Container if it doesn’t.

src/tinyReact/diff.js

import mountElement from './mountElement';

function diff (virtualDOM, container, oldDOM) {
    // Check whether oldDOM is patrolling
    if(! oldDOM) {returnmountElement(virtualDOM, container); }}Copy the code

Determine whether the virtual DOM you want to transform is a component or a generic label. In this case, we will default to only native JSX tags and call the mountNativeElement method.

src/tinyReact/mountElement.js

import mountNativeElement from './mountNativeElement';

function mountElement(virtualDOM, container) {
    // Handle native JSX and component JSX
    mountNativeElement(virtualDOM, container);
}
Copy the code

The mountNativeElement file is used to convert the native virtual DOM into the real DOM by calling the createDOMElement method.

src/tinyReact/mountNativeElement.js

import createDOMElement from './createDOMElement';

function mountNativeElement(virtualDOM, container) {
    // Convert the virtual DOM to a real object
    let newElement = createDOMElement(virtualDOM);
    // Place the transformed DOM object on the page
    container.appendChild(newElement);
}
Copy the code

Methods to create the real DOM are defined separately for easy reuse. You need to determine if it is an element node and if it is a text node, create the corresponding element. The child nodes are then created recursively. Finally, place the node we created in the specified container.

src/tinyReact/createDOMElement.js

import mountElement from "./mountElement";

function createDOMElement(virtualDOM) {
    let newElement = null;
    if (virtualDOM.type === 'text') {
        // The text node is created using createTextNode
        newElement = document.createTextNode(virtualDOM.props.textContent);
    } else {
        // The element node is created with createElement
        newElement = document.createElement(virtualDOM.type);
    }
    // Create child nodes recursively
    virtualDOM.children.forEach(child= > {
        mountElement(child, newElement);
    })
    return newElement;
}
Copy the code

Add attributes to the real DOM object

We know that the properties are stored in the props of the virtual DOM, so we just need to loop through the properties when we create the element and put them in the real element.

There are different circumstances to consider when adding attributes, such as the event and static attributes are different, and the methods used to add attributes are different. Boolean and value attributes are set differently. We also need to determine whether the attribute is children, because children is not an attribute, it is a child element that we define ourselves. If the attribute is className, we need to convert it to class to add it.

src/tinyReact/createDOMElement.js

We have a separate method to add attributes to the element, which is called updateNodeElement after the element is created

import mountElement from "./mountElement";
import updateNodeElement from "./updateNodeElement";

function createDOMElement(virtualDOM) {
    let newElement = null;
    if (virtualDOM.type === 'text') {
        // The text node is created using createTextNode
        newElement = document.createTextNode(virtualDOM.props.textContent);
    } else {
        // The element node is created with createElement
        newElement = document.createElement(virtualDOM.type);
        // Call the method to add attributes
        updateNodeElement(newElement, virtualDOM)
    }
    // Create child nodes recursively
    virtualDOM.children.forEach(child= > {
        mountElement(child, newElement);
    })
    return newElement;
}
Copy the code

You first need to get the property list of the node Object, use object.keys to get the property name, and then use forEach to iterate over it.

src/tinyReact/updateNodeElement.js

If the property name starts with on we think it’s an event, and then we intercept the event name which means we remove the on at the beginning and lower case the string, and we bind the event using addEventListener.

If the property name is value or Checked cannot be set using setAttribute, the property name equals the value of the property.

If the attribute name is className, convert it to class. If the attribute name is not children, all other attributes can be set using setAttribute.

function updateNodeElement(newElement, virtualDOM) {
    // Get the property object corresponding to the node
    const newProps = virtualDOM.props;
    Object.keys(newProps).forEach(propName= > {
        const newPropsValue = newProps[propName];
        // Check whether it is an event attribute
        if (propName.startsWith('on')) {
            // Intercepts the event name
            const eventName = propName.toLowerCase().slice(2);
            // Add events to the element
            newElement.addEventListener(eventName, newPropsValue);
        } else if (propName === 'value' || propName === 'checked') {
            // If the attribute name is value or checked cannot be set using setAttribute, set it as an attribute
            newElement[propName] = newPropsValue;
        } else if(propName ! = ='children') {
            / / out of children
            if (propName === 'className') {
                newElement.setAttribute('class', newPropsValue)
            } else {
                newElement.setAttribute(propName, newPropsValue)
            }
        }
    })
}
Copy the code

Component rendering – Distinguishes between functional components and class components

The first thing we need to make clear before rendering a component is that the component’s virtual DOM type value is a function, and this is true for both function and class components.

const Head = () = > <span>head</span>
Copy the code

The virtual DOM of the component

{
    type: function(){},
    props: {},
    children: []}Copy the code

When rendering components, we should first distinguish Component from Native Element. If it is a Native Element, it can be directly rendered, which we have dealt with before. If it is a Component, special processing is required.

We can render a component in the entry file SRC /index.js.

import TinyReact from "./TinyReact"

const root = document.getElementById('root');

function Demo () {
    return <div>hello</div>
}
function Head () {
  return <div><Demo /></div>
}

TinyReact.render(<Head />, root);
Copy the code

You then need to distinguish between native tags and components in the mountElement method.

src/tinyReact/isFunction.js

function isFunction(virtualDOM) {
    return virtualDOM && typeof virtualDOM.type === 'function';
}
Copy the code

We handle components in the mountComponent method. First we need to consider whether this component is a class component or a function component, since they are handled differently, using whether the render function exists on the prototype. We can use the isFunctionComponent function to determine this

src/tinyReact/mountComponent.js

If type exist, and is a function object, and the object does not exist on the render method, that is a function component/SRC/tinyReact isFunctionComponent. Js

import isFunctionComponent from './isFunctionComponent';

function mountComponent(virtualDOM, container) {
    // Determine whether the component is a class component or a function component
    if (isFunctionComponent(virtualDOM)) {
        
    }
}
Copy the code

src/tinyReact/isFunctionComponent.js

import isFunction from "./isFunction";

function isFunctionComponent(virtualDOM) {
    const type = virtualDOM.type;
    returntype && isFunction(virtualDOM) && ! (type.prototype && type.prototype.render) }Copy the code

Processing function component

Let’s start with the function component, which is actually quite simple. You just call the type function to get the returned virtual DOM. After retrieving the virtual DOM, we need to determine whether the newly acquired virtual DOM is a component. If we continue to call mountComponent, if not, we call the mountNativeElement method directly to render the virtual DOM to the page for the native DOM element.

src/tinyReact/mountComponent.js

import isFunction from './isFunction';
import isFunctionComponent from './isFunctionComponent';
import mountNativeElement from './mountNativeElement';

function mountComponent(virtualDOM, container) {
    // Store the resulting virtual DOM
    let nextVirtualDOM = null;
    // Determine whether the component is a class component or a function component
    if (isFunctionComponent(virtualDOM)) {
        // Handle function components
        nextVirtualDOM = buildFunctionComponent(virtualDOM);
    }
    // Determine if it is still a function component
    if (isFunction(nextVirtualDOM)) {
        mountComponent(nextVirtualDOM, container);
    }
    / / render nextVirtualDOM
    mountNativeElement(nextVirtualDOM, container);
}

function buildFunctionComponent (virtualDOM) {
    return virtualDOM.type();
}
Copy the code