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

Function the props property of the component

We can pass in a title parameter when rendering the Head component.

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

function Head (props) {
  return <div>
    {props.title}
  </div>
}

TinyReact.render(<Head title="hello" />, root);
Copy the code

On the component, there is a props parameter. Inside the component, we can get this value on props. When we render a function component, we can call the component function in buildFunctionComponent. We can pass in props when we call it. So we’re going to be compatible with null objects here.

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

Class component rendering

Here we create a class Component. In React, the class Component inherits from the Component class. We can create both of them.

src/index.js

class Alert extends TinyReact.Component {
  render() {
    return <div>Hello Class Component</div>
  }
}

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

src/tinyReact/Component.js

export default class Component {}Copy the code

Once we’re done we need to render the class component, again in mountComponent.js, where we’ve already implemented the render function component.

if (isFunctionComponent(virtualDOM)) {
    // Handle function components
    nextVirtualDOM = buildFunctionComponent(virtualDOM);
} else {
    // Process the class component
}
Copy the code

We create a buildClassComponent method to handle class components. This function takes the virtual DOM. In this function we need to get the instance object of the component, because we can only get the Render method if we get the instance object. You get the virtual DOM object that the component outputs by calling the Render method.

// Process the class component
function buildClassComponent (virtualDOM) {
    // Get the instance object
    const component = new virtualDOM.type();
    // Get the virtual DOM object
    const nextVirtualDOM = component.render();
    return nextVirtualDOM;
}
Copy the code

The rest of the logic is the same as the function component to determine whether the returned DOM is a component DOM or a native DOM. If the COMPONENT DOM is passed recursively to mountComponent, if the native DOM is called mountNativeElement for rendering.

Class component props processing

We know that in the class Component we can pass the parameter this.props. Our class Component is integrated with the Component parent class. We can call the methods of the parent class and make the props in the parent class equal to the props passed in, so that the subclass can get props.

We add a constructor in the subclass that accepts props, and then calls the super parent class, passing props to the parent class.

class Alert extends TinyReact.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return <div>{this.props.name} {this.props.age}</div>}}Copy the code

In the constructor of the parent class, get props and assign to the props property. So the subclass inherits the parent class, and the subclass has this property.

class Component {
    constructor(props) {
        this.props = props; }}Copy the code

Finally, we pass in props when we instantiate the component.

function buildClassComponent (virtualDOM) {
    // Get the instance object
    const component = new virtualDOM.type(virtualDOM.props || {});
    // Get the virtual DOM object
    const nextVirtualDOM = component.render();
    return nextVirtualDOM;
}
Copy the code

Update the DOM element – text node

To update the DOM elements in the page, virtual DOM comparison is used. The new virtual DOM and the old virtual DOM are compared to find out the differences, and the differences are updated to the page to achieve the minimal update of DOM.

In the virtual DOM comparison, need to use the updated virtual DOM and the virtual DOM before the update, the updated virtual DOM we can pass through the render method, now the problem is how to obtain the virtual DOM before the update.

For virtual DOM updates before, the corresponding is already on the page show real DOM, in this case, so we create real DOM object, you can add virtual DOM to the real properties of DOM object, before the virtual DOM comparison, can be in real DOM object to obtain the corresponding virtual DOM object, Container. FirstChild is obtained by passing the third argument to the Render method.

The first step is to add the corresponding virtual DOM object to the real DOM. We can find the real DOM object we created in the createDOMElement method and add a _virtualDOM attribute to it to store the corresponding virtualDOM.

 // Add virtual DOM attributes
newElement._virtualDOM = virtualDOM;
Copy the code

In the Render method we originally defined we actually passed three parameters, the current virtual DOM object, the container object to render to, and the old virtual DOM object.

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

The third argument is actually not passed in by the Render method, but is retrieved from the page, and should be the container.firstChild object. That is, the content object being rendered in the current container. Since we all know that JSX code must have a package tag, which means that containers can only have one child element, we use firstChild.

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

Then we can go to the old virtual DOM object in oldDOM, which we get first in the diff algorithm.

const oldVirtualDOM = oldDOM && oldDOM._virtualDOM;
Copy the code

And then we can compare. If oldVirtualDOM exists, we first compare the tag type of the two elements. If the two elements have the same type, we need to determine whether the text type node or the element type node. The text type updates the content directly, while the element type updates the tag attribute.

import mountElement from './mountElement';
import updateTextNode from './updateTextNode';

function diff (virtualDOM, container, oldDOM) {
    // Get the old virtual DOM object
    const oldVirtualDOM = oldDOM && oldDOM._virtualDOM;
    // Check whether oldDOM is patrolling
    if(! oldDOM) {return mountElement(virtualDOM, container);
    } else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
        // Both elements are of the same type
        // The text type updates the content directly
        // The element type updates the attribute of the tag
        if (virtualDOM.type === 'text') {
            // Update the content
            updateTextNode(virtualDOM, oldVirtualDOM, oldDOM);

        } else {
            // Update element attributes
        }
        // Iterate over the child elements for comparison
        virtualDOM.children.forEach((child, i) = >{ diff(child, oldDOM, oldDOM.childNodes[i]); }}})Copy the code

Again, here we pull out an update method. In this method, determine if the content is the same and update it if not.

src/tinyReact/updateTextNode.js

function updateTextNode (virtualDOM, oldVirtualDOM, oldDOM) {
    if(virtualDOM.props.textContent ! == oldVirtualDOM.props.textContent) {// Update DOM node content
        oldDOM.textContent = virtualDOM.props.textContent;
        // Update the old virtual DOMoldDOM._virtualDOM = virtualDOM; }}Copy the code

Update DOM element-node attributes

In fact, it is to compare the old and new node property objects, find the difference part, and then update the difference part to the node property. We use the previously defined updateNodeElement method, which we implemented to update the element’s attributes.

if (virtualDOM.type === 'text') {
    // Update the content
    updateTextNode(virtualDOM, oldVirtualDOM, oldDOM);

} else {
    // Update element attributes
    // Which element to update, the updated virtual DOM, the old virtual DOM
    updateNodeElement(oldDOM, virtualDOM, oldVirtualDOM)
}
Copy the code

Next we modify the updateNodeElement method to add the oldVirtualDOM parameter. OldVirtualDOM does not always exist, it only exists when it is updated, so it needs to be compatible with empty state.

When you recycle the new property object, you can get the property name. You can compare the property name with the old property value to see if the two property values are the same.

function updateNodeElement(newElement, virtualDOM, oldVirtualDOM) {
    // Get the property object corresponding to the node
    const newProps = virtualDOM.props || {};
    // Get the old property object
    const oldProps = oldVirtualDOM.props || {};
}
Copy the code

Compare the two values to see if they are the same, and update them if they are not. In the update operation, the event needs to be noted and the original event is cleared.

// If existing events exist, delete them.
if (oldPropsValue) {
    newElement.addEventListener(eventName, oldPropsValue);
}
Copy the code

If any properties are deleted, you need to delete the properties on the DOM object. We can loop through the oldProps, and if it is not in the newProps, it is deleted.

// Determine if the attribute is deleted
Object.keys(oldProps).forEach(propName= > {
    // New attribute value
    const newPropsValue = newProps[propName];
    // Old attribute value
    const oldPropsValue = oldProps[propName];
    if(! newPropsValue) {// Check whether it is an event attribute
        if (propName.startsWith('on')) {
                // Intercepts the event name
                const eventName = propName.toLowerCase().slice(2);
                // Delete the event
                newElement.removeEventListener(eventName, oldPropsValue);
        } else if(propName ! = ='children') { newElement.removeAttribute(propName); }}})Copy the code

The virtual DOM has the same type. If it is an element node, it compares whether the attribute of the element node has changed; if it is a text node, it compares whether the content of the text node has changed. To achieve the comparison, you need to first obtain the corresponding virtual DOM object from the existing DOM object.

const oldVirtualDOM = oldDOM && oldDOM._virtualDOM
Copy the code

Check whether oldVirtualDOM exists. If so, continue to check whether the virtual DOM type to be compared is the same. If the type is the same, check whether the node type is text. The updateNodeElement method is called for element node comparisons.

else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
    // Both elements are of the same type
    // The text type updates the content directly
    // The element type updates the attribute of the tag
    if (virtualDOM.type === 'text') {
        // Update the content
        updateTextNode(virtualDOM, oldVirtualDOM, oldDOM);

    } else {
        // Update element attributes
        // Which element to update, the updated virtual DOM, the old virtual DOM
        updateNodeElement(oldDOM, virtualDOM, oldVirtualDOM)
    }
}
Copy the code

It’s always the topmost element, and when it’s done, you have to recursively compare the child elements

// Iterate over the child elements for comparison
virtualDOM.children.forEach((child, i) = > {
    diff(child, oldDOM, oldDOM.childNodes[i]);
})
Copy the code

If the two node types are different, there is no need to compare them, just use the new virtual DOM to generate a new DOM object, and replace the old DOM object.

Use else if in diff.js to handle this case.

if(! oldDOM) {return mountElement(virtualDOM, container);
} else if(virtualDOM.type ! == oldVirtualDOM.type &&typeofvirtualDOM.type ! = ='function') {
    // If the label type is different and is not a component.
    const newElement = createDOMElement(virtualDOM);
    // Replace the DOM element
    oldDOM.parentNode.replaceChild(newElement, oldDOM);
} else if
Copy the code

Remove nodes

Deletion of nodes occurs after the node is updated, and occurs to all children of the same parent node. After the node is updated, if the number of old node objects exceeds the number of new virtual DOM nodes, it indicates that a node needs to be deleted.

Here we get the number of DOM nodes. If the number of old and new DOM nodes is different, we loop through the old DOM nodes and delete them from the beginning until the number of old and new DOM nodes is the same.

// Delete a node
// Get the old node
const oldChildNodes = oldDOM.childNodes;
// Determine the number of old nodes
if (oldChildNodes.length > virtualDOM.children.length) {
    // Loop to delete nodes
    for (let i = oldChildNodes.length - 1; i > virtualDOM.children.length -1; i--) { oldDOM.removeChild(oldChildNodes[i]); }}Copy the code

Because the previous update method has ensured that the corresponding nodes are the same, the redundant nodes can be deleted directly, and there is no need to worry about the problem of unsynchronization of the remaining nodes.

Class component status updates

To update the class component, we need to implement the setState method. We first define an Alert component, which updates the title property in the component and updates it to the page when the button is clicked.

class Alert extends TinyReact.Component {
  constructor(props) {
    super(props);
    this.state = {
      title: 'default'
    }
    this.handileClick = this.handileClick.bind(this);
  }

  handileClick() {
    this.setState({
      title: 'Changed'})}render() {
    return <div>
      <div>{this.state.title}</div>
      <button 
      onClick={()= >{ this.handileClick(); }} > change the Title</button>
    </div>}}Copy the code

The setState we call here should be setState in the parent Component class. When a subclass calls setState, it must first make clear that this in setState refers to an instance of the subclass.

setState (state) {
    this.state = Object.assign({}, this.state, state);
}
Copy the code

When the state changes, we need to trigger the render method again. When the state changes, we need to update the state in the page. We can use render to get the latest virtual DOM, and then compare and update the old virtual DOM.

Comparison here is more troublesome, call render method we can get the current virtual DOM, but can not get the page display DOM, we can define a setDOM method to store the page display DOM. Pass the class component to setDOM when it is instantiated. Then call the diff method to update the comparison.

class Component {
    constructor(props) {
        this.props = props;
    }
    setState (state) {
        this.state = Object.assign({}, this.state, state);
        // Get the latest DOM object
        const virtualDOM = this.render();
        // Get the old virtualDOM object for comparison
        const oldDOM = this.getDOM();
        // Implement comparisons
        diff(virtualDOM, container, oldDOM);
    }
    setDOM (dom) { // Store the DOM objects displayed on the page
        this._dom = dom;
    }
    getDOM () { // Get the DOM displayed on the page
        return this._dom; }}Copy the code

Component update function

The same component may be rendered or a different component may be rendered when a component is updated, and we need to take care of both.

First of all, we need to determine whether the virtual DOM to be updated is a component in the diff. If the component is judging whether the component to be updated is the same as the component before it is updated, if it is not the same component, there is no need to do component update operation, and directly call the mountElement method to add the virtual DOM returned by the component to the page.

If it is the same component, the component update operation is performed, which is to pass the latest props to the component, call the render method of the component to get the latest virtual DOM object returned by the component, pass the virtual DOM object to the DIff method, and let the DIff method find the differences and update the differences to the real DOM object.

Different lifecycle functions are called at different stages of the component update process.

We first determine whether the virtual DOM to be updated is a component in the diff method.

If the component is divided into multiple cases, the diffComponent method is added to handle this. This method takes four parameters, the first parameter is the component of virtual DOM object itself, it allows you to get to the components of the latest props, the second parameter is to update the component instance of the object, through the function, it can call the component life cycle can be updated components of props attribute, you can get the latest virtual DOM object to the component returns, The third parameter is the DOM object to update. When updating the component, you need to modify the existing DOM object to achieve DOM minimization and retrieve the old virtual DOM object. The fourth parameter is the container element to update to.

else if (typeof virtualDOM.type === 'function') {
    // Render is a component
    diffComponent(virtualDOM, oldComponent, oldDOM, container);
} else if

Copy the code

In the diffComponent we need to check whether virtualDOM and oldComponent are the same component by checking whether their constructors are the same.

function diffComponent(virtualDOM, oldComponent, oldDOM, container) {
    if (isSameComponent(virtualDOM, oldComponent)) {
        // It is the same component
    } else {
        // Not the same component
        // Replace the original object of the page, that is, remove the original DOM, add a new DOMmountElement(virtualDOM, container, oldDOM); }}function isSameComponent(virtualDOM, oldComponent) {
    // Check whether they are the same component by checking whether their constructors are the same
    return oldComponent && virtualDOM.type === oldComponent.constructor;
}
Copy the code

Replace the original component if it is not the same component. You need to receive oldDOM in mountNativeElement and then delete the DOM.

function mountNativeElement(virtualDOM, container, oldDOM) {
    // Convert the virtual DOM to a real object
    // Determine whether the old DOM object exists and delete it if it does
    if (oldDOM) {
        unmountNode(oldDOM);
    }
    let newElement = createDOMElement(virtualDOM);
    // Place the transformed DOM object on the page
    container.appendChild(newElement);
    // Get the instance object
    const component = virtualDOM.component;
    if(component) { component.setDOM(newElement); }}Copy the code

If the component that needs to be updated is the same component as the old one, we use the updateComponent method. Pass in virtualDOM, container, oldDOM, and container.

function diffComponent(virtualDOM, oldComponent, oldDOM, container) {
    if (isSameComponent(virtualDOM, oldComponent)) {
        // It is the same component
        updateComponent(virtualDOM, container, oldDOM, container);
    } else {
        // Not the same component
        // Replace the original object of the page, that is, remove the original DOM, add a new DOMmountElement(virtualDOM, container, oldDOM); }}Copy the code

The thing to do in this method is component update. We need to update the props property of the component. The new props is stored in the virtualDOM props. We need to pass the props method through the oldComponent instance to update the props.

We need to define the props method in the comonent. js class, updateProps, and receive a props.

updateProps(props) {
    this.props = props;
}
Copy the code

We can then update the props by calling the updateProps method with oldComponent.

oldComponent.updateProps(virtualDOM.props);
Copy the code

After the update we need to get the latest virtual DOM. Then the diff algorithm is used for comparison and update. Don’t forget to assign the updated instance to the new virtual DOM instance for later use.

function updateComponent(virtualDOM, oldComponent, oldDOM, container) {
    // Component update
    oldComponent.updateProps(virtualDOM.props);
    // Get the latest virtual DOM,
    let nextVirtualDOM = oldComponent.render();
    // Update the instance
    nextVirtualDOM.component = oldComponent;
    // diff respectively and update.
    diff(nextVirtualDOM, container, oldDOM)
}
Copy the code

Component life cycle

We also need to call the Component’s lifecycle function during Component updates, which we add to the Component class by default.

componentWillMount() {}
componentDidMount() {}
componentWillReceviceProps(nextProps) {}
shouldComponentUpdate(nextProps, nextState) {
    returnnextProps ! = =this.props || nextState ! = =this.state;
}
componentWillUpdate(nextProps, nextState) {}
componentDidUpdate(prevProps, preState) {}
componentWillUnmount() {}
Copy the code

In updateComponent this function, we should first call componentWillReceviceProps life cycle, the life cycle of the call to the incoming new props.

oldComponent.componentWillReceviceProps(virtualDOM.props);
Copy the code

We then call shouldComponentUpdate life cycle to determine if the component needs to be updated.

if (oldComponent.shouldComponentUpdate(virtualDOM.props)) {
    // Component update
    oldComponent.updateProps(virtualDOM.props);
    // Get the latest virtual DOM,
    let nextVirtualDOM = oldComponent.render();
    // Update the instance
    nextVirtualDOM.component = oldComponent;
    // diff respectively and update.
    diff(nextVirtualDOM, container, oldDOM)
}
Copy the code

Then you call the componentWillUpdate life cycle.

// Lifecycle
oldComponent.componentWillUpdate(virtualDOM.props);
Copy the code

To implement the componentDidUpdate lifecycle after the component is updated, pass in props before the update. We can define a variable to store in advance.

function updateComponent(virtualDOM, oldComponent, oldDOM, container) {
    // Lifecycle
    oldComponent.componentWillReceviceProps(virtualDOM.props);
    // Determine whether to update the life cycle
    if (oldComponent.shouldComponentUpdate(virtualDOM.props)) {
        // Store props before the update
        let prevProps = oldComponent.props;
        // Lifecycle
        oldComponent.componentWillUpdate(virtualDOM.props);
        // Component update
        oldComponent.updateProps(virtualDOM.props);
        // Get the latest virtual DOM,
        let nextVirtualDOM = oldComponent.render();
        // Update the instance
        nextVirtualDOM.component = oldComponent;
        // diff respectively and update.
        diff(nextVirtualDOM, container, oldDOM)
        // LifecycleoldComponent.componentDidUpdate(prevProps); }}Copy the code

Implement ref function

Adding the ref attribute to a node can fetch the DOM object of that node. For example, in Demo, the ref attribute was added to the P tag to fetch the p element object and retrieve the contents of p when the button is clicked.

class Demo extends TinyReact.Component {
  constructor(props) {
    super(props);
    this.state = {
      title: 'default'}}}render() {
    return <div>
      <div>{this.state.title}</div>
      <p ref={p= > this.p = p}>{this.props.name}</p>
      <button 
      onClick={()= >{ // this.handileClick(); console.log(this.p.innerText); }}> Get the content</button>
    </div>
  }
}

TinyReact.render(<Demo name="yindong"/>, root);
Copy the code

And more simple to achieve, in creating virtual DOM object when node to determine its existence ref attribute, if there is a call the ref attribute will be stored in method and to create the DOM object passed as a parameter to ref method, so that at the time of rendering component node can get object element and the element object is stored as the component properties. Add it in the createDOMElement method.

if (virtualDOM.props && virtualDOM.props.ref) {
    virtualDOM.props.ref(newElement);
}
Copy the code

A REF attribute can also be added to a class component to get an instance object of the component. In the mountComponent method, if you are dealing with a class component, get the instance object from the dummy DOM object returned by the class component, look for ref in the props property of the instance object, and if it exists, call ref and pass the argument to the instance object.

And here we can also add the componentDidMount lifecycle function.

// Used to store instance objects
let component = null;
// Determine whether the component is a class component or a function component
if (isFunctionComponent(virtualDOM)) {
    // Handle function components
    nextVirtualDOM = buildFunctionComponent(virtualDOM);
} else {
    // Process the class component
    nextVirtualDOM = buildClassComponent(virtualDOM);
    component = nextVirtualDOM.component;
}
if (isFunction(nextVirtualDOM)) {
    mountComponent(nextVirtualDOM, container);
}
if (component) {
    component.componentDidMount();
}
/ / ref
if (component && component.props && component.props.ref) {
    omponent.props.ref(component);
}
Copy the code

Key property implementation

In React, when rendering type data, a key attribute is usually added to the rendered list element. The key attribute is a unique identifier of the data, which helps React identify which elements have been modified or deleted, thus minimizing DOM operations.

The key attribute does not need to be globally unique, but must be unique between siblings under the same parent node. That is, the key attribute is used to compare children of the same type under the same parent node.

The way to do this in the previous tutorial is to delete nodes from back to front, leaving the previous nodes the same and removing the extra nodes. In fact, this is very inefficient, the correct approach is to find the nodes do not need to delete directly. You can do this using the key attribute.

When two elements are compared, if they are of the same type, the child element of the old DOM object is looped to check whether it has the key attribute. If so, the DOM object of this child element is stored in a JavaScirpt object, and then the child element of the virtual DOM object to be rendered is looped. In the loop, we get the key attribute of the child element, and then use the key attribute to look up the DOM object in the JavaScript object. If the element can be found, it means that the element already exists and does not need to be re-rendered. If the element cannot be found through the key attribute, it means that the element is new.

Start adding this functionality in the DIff algorithm.

// Place the child element with the key attribute in a separate object
const keyedElements = {};
for (let i = 0, len = oldDOM.childNodes.length; i < len; i++) {
    let domElement = oldDOM.childNodes[i];
    // Check the node type
    if (domElement.nodeType === 1) {
        const key = domElement.getAttribute('key')
        if(key) { keyedElements[key] = domElement; }}}Copy the code

Loop through the child element of the virtual DOM to render, get the key attribute of the child element, and check to see if the element exists, and if so, to see if the element in its current position is the one we expect. If not, insert it here.

// Loop through the child element of the virtual DOM to render, and get the key attribute of the child element
virtualDOM.children.forEach((child, i) = > {
    const key = child.props.key;
    if (key) {
        const domElement = keyedElements[key];
        if (domElement) {
            // Check if the element in the current position is the one we expect, if not, insert it there
            if(oldDOM.childNodes[i] && oldDOM.childNodes[i] ! == domElement ) { oldDOM.insertBefore(domElement, oldDOM.childNodes[i]); }}}})Copy the code

We also need to judge whether there are elements in keyedElements. If there are no elements, there is no key, so we can make comparison by index, and if there are keys, we can make comparison by key.

let hasNoKey = Object.keys(keyedElements).length === 0;

if (hasNoKey) {
    // Iterate over the child elements for comparison
    virtualDOM.children.forEach((child, i) = >{ diff(child, oldDOM, oldDOM.childNodes[i]); })}else {
    // Loop through the child element of the virtual DOM to render, and get the key attribute of the child element
    virtualDOM.children.forEach((child, i) = > {
        const key = child.props.key;
        if (key) {
            const domElement = keyedElements[key];
            if (domElement) {
                // Check if the element in the current position is the one we expect, if not, insert it there
                if(oldDOM.childNodes[i] && oldDOM.childNodes[i] ! == domElement) { oldDOM.insertBefore(domElement, oldDOM.childNodes[i]); }}}})}Copy the code

Then we have to deal with the DOM element that is not found through the key attribute. If it is not found, it is new and can be rendered directly to the page using the mountElement method.

if (key) {
    const domElement = keyedElements[key];
    if (domElement) {
        // Check if the element in the current position is the one we expect, if not, insert it there
        if(oldDOM.childNodes[i] && oldDOM.childNodes[i] ! == domElement) { oldDOM.insertBefore(domElement, oldDOM.childNodes[i]); }}}else {
    // Add a new element
    mountElement(child, oldDOM, oldDOM.childNodes[i])
}
Copy the code

We need to determine in the mountNativeElement method that if oldDOM exists, we should use the container.insertbefore method to insert it before oldDOM, and appendChild to the end if it doesn’t.

let newElement = createDOMElement(virtualDOM);
if (oldDOM) {
    container.insertBefore(newElement, oldDOM);
} else {
    container.appendChild(newElement);
}
Copy the code

Uninstall the node

In the process of node comparison, if the number of old nodes is more than the number of new nodes to be rendered, it indicates that some nodes have been deleted. Continue to check whether there are elements in the keyedElements object. If there are no elements, use the index method to delete them; if there are, use the key attribute method to delete them.

Implementation approach is recycling old node, in the process of recycling old node to obtain the old node corresponding key attributes, and then according to the key attribute to find the old node in the new node, if found that this node has not been deleted, if not found that node is deleted, call uninstall node method can delete node.

We determine if hasNoKey has a key when diff deletes the node.

// Delete a node
// Get the old node
const oldChildNodes = oldDOM.childNodes;
// Determine the number of old nodes
if (oldChildNodes.length > virtualDOM.children.length) {
    if (hasNoKey) {
        // Loop to delete nodes
        for (let i = oldChildNodes.length - 1; i > virtualDOM.children.length - 1; i--) { unmountNode(oldChildNodes[i]); }}else {
        // Delete a node using the key attribute
        // Delete the old key from the new key
        for (let i = 0; i < oldChildNodes.length; i++) {
            const oldChild = oldChildNodes[i];
            const oldChildKey = oldChild._virtualDOM.props.key;
            let found = false;
            for (let n = 0; n < virtualDOM.children.length; n++) {
                if (oldChildKey === virtualDOM.children[n].props.key) {
                    found = true;
                    break; }}if(! found) { unmountNode(oldChild); }}}}Copy the code

, of course, is not to say that the unloading node delete directly is ok, also need to consider the following situation, if you want to delete the node is a text node can be directly deleted, if the component is generated, you need to call the component’s uninstall life cycle function, if the node contains the other components generated nodes, you need to call other components uninstall life cycle, You also need to remove the DOM node object passed to the component through the REF attribute if the node has the ref attribute on it, and remove the event if it has an event. We can handle these cases in the unmountNode function.

If it is a text node, delete it.

function unmountNode(node) {
    // Get the virtual DOM object
    const virtualDOM = node._virturalDOM;
    // Delete the text node directly
    if (virtualDOM.type === 'text') {
        node.remove();
        return; }}Copy the code

If it is not a text node, you need to check whether the node was generated by a component. If it is generated by a component, you need to call the component’s unload life cycle.

// Determine whether the node is component generated.
const component = virtualDOM.component;
if (component) {
    component.componentWillUnmount();
}
Copy the code

You also need to determine whether the node has the REF attribute, and if so, clean it up

// Determine whether the node has the ref attribute, and if so, clear it
if (virtualDOM.props && virtualDOM.props.ref) {
    virtualDOM.props.ref(null)}Copy the code

Also determine whether there are events on the node, and if there are events that need to be uninstalled

// Check whether the event exists
Object.keys(virtualDOM.props).forEach(propsName= > {
    if (propsName.startsWith('on')) {
        const eventName = propsName.toLocaleLowerCase().slice(0.2);
        consteventHandler = virtualDOM.props[propsName]; node.removeEventListener(eventName, eventHandler); }})Copy the code

Determine if there are children in the node, and if there are, delete them recursively, because children also need to determine the above.

// Delete child nodes recursively
if (node.childNodes.length > 0) {
    for (let i = 0; i < node.childNodes.length; i++) { unmountNode(node.childNodes[i]); }}Copy the code

Finally, we need to execute node.remove to remove the current node.

// Delete a node
node.remove();
Copy the code

So now we delete the DOM node and we’re done here.

function unmountNode(node) {
    // Get the virtual DOM object
    const virtualDOM = node._virtualDOM;
    // Delete the text node directly
    if (virtualDOM.type === 'text') {
        node.remove();
        return;
    }
    // Determine whether the node is component generated.
    const component = virtualDOM.component;
    if (component) {
        component.componentWillUnmount();
    }
    // Determine whether the node has the ref attribute, and if so, clear it
    if (virtualDOM.props && virtualDOM.props.ref) {
        virtualDOM.props.ref(null)}// Check whether the event exists
    Object.keys(virtualDOM.props).forEach(propsName= > {
        if (propsName.startsWith('on')) {
            const eventName = propsName.toLocaleLowerCase().slice(0.2);
            consteventHandler = virtualDOM.props[propsName]; node.removeEventListener(eventName, eventHandler); }})// Delete child nodes recursively
    if (node.childNodes.length > 0) {
        for (let i = 0; i < node.childNodes.length; i++) { unmountNode(node.childNodes[i]); }}// Delete a node
    node.remove();
}
Copy the code