React source code analysis-TinyReact analysis

React source code analysis-TinyReact analysis

Based on TinyReact, analyzing the main principles of React, the React framework is compatible with too many businesses, and the code volume is huge and it is difficult to read. This article selects TinyReact which contains the main realization ideas of the React framework.

What is JSX

Understand that JSX plays an important role in understanding the virtual DOM. JSX just looks like HTML, but it is JavaScript. Babel will compile JSX into React API before the React code is executed.

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

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

jsx will be converted into React.createElement object by Babel at runtime, React.createElement will be converted into virtual DOM object by React, and virtual DOM object will be converted into real DOM object by React.

The emergence of JSX syntax is to make it easier for React developers to write user interface code.

What is virtual DOM

In React, each DOM object has a corresponding virtual DOM object, which is the JavaScript representation of the DOM object. In fact, it uses JavaScript objects to describe the DOM object information, such as the type of DOM object and what attributes it has. , Which child elements it has.

The virtual DOM object can be understood as a copy of the DOM object, but the virtual DOM cannot be displayed directly on the screen. Virtual DOM is to solve the performance problem of React operating DOM.

//Before compilation <div className = "content" > < h3 > Hello React </h3 > < p > React is great </p > </div> //after compilation { 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 code

React uses minimal DOM operations to enhance the advantages of DOM operations. Only updates that need to be updated. When React creates a DOM object for the first time, it creates a virtual DOM object for each DOM object. React will do it before the DOM object is updated. Update all virtual DOM objects, then compare the virtual DOM before the update and the virtual DOM after the update, find the changed DOM object, and only update the changed DOM to the page, which improves the performance of js operating the DOM.

Although the virtual DOM update and comparison operations are performed before operating the real DOM, because JS operates its own objects efficiently, the cost is almost negligible.

Before the React code is executed, JSX will be converted by Babel into a call to the React.createElement method. When the createElement method is called, the type of the element, the attributes of the element, and the child elements of the element are passed in. The return value of the createElement method is the constructed one. Virtual DOM object. Here we implement a createElement method by ourselves.

The createElement method receives three parameters: type, props, and childrens. Represents the tag type, tag attributes and tag sub-elements respectively. In this method, a virtual DOM object is to be returned. In this object, a type attribute is actually the value passed in by the parameter, followed by props and children.

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

We use TinyReact here to analyze React code. 1. configure babel to compile jsx into Tiny's createElement method, which is convenient for us to debug

.babelrc

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

Scaffolding warehouse self-created address link

src/index.js

import TinyReact from "./TinyReact" const virtualDOM = ( < div className = "container" > < h1 > Hello I am virtual DOM </h1 > </div > ) console .log(virtualDOM); Copy code

The console prints the results.

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

Here we print out a simple virtual DOM, but there is also a problem. The text node "Hello, I am the virtual DOM" is directly added to the children array as a string. This is incorrect. The correct approach should be a text node. It should also be a virtual DOM object.

We only need to loop through the children array, and if it is not an object, we will consider it a text node, and we will replace it with an object.

function createElement ( type, props, ...children ) { //Traverse the children object const childElements = [].concat(...children).map( child => { if (child instanceof Object ) { return child;//The object returns directly } else { //Not the object calls createElement method to generate an object return createElement( 'text' , { textContent : child }); } }) return { type, props, children : childElements } } Copy code

The text node becomes an object.

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

We all know that if it is a boolean or null value in the component template, the node is not displayed. We need to deal with it here.

<div className= "container" > < h1 > Hello, I am a virtual DOM </h1 > { 1 === 2 && < h1 > Boolean value node </h1 > } </div> Copy code
function createElement ( type, props, ...children ) { //Traverse the children object const childElements = [].concat(...children).reduce( ( result, child ) => { //Determine whether child cannot be a boolean Cannot be null //Because reduce is used, result is the return value of the previous loop, and finally return result can be if (child !== false && child !== true && child !== null ) { if (child instanceof Object ) { result.push(child); //It is an object that returns directly } else { //It is not an object that calls createElement method to generate an object result.push(createElement( 'text' , { textContent : child })); } } return result; }, []) return { type, props, children : childElements } } Copy code

We also need to put children into props, just use Object.assign to merge props and children back.

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

Convert virtual DOM to real DOM

We have to define a render method.

src/tinyReact/render.js

This method needs to receive three parameters, the first parameter is the virtual DOM, the second parameter is the page element to be rendered, and the third parameter is the old virtual DOM for comparison. The main function of the render method is to convert the virtual DOM into a real DOM and render it to the page.

import diff from './ diff ' function render ( virtualDOM, container, oldDOM ) { diff(virtualDOM, container, oldDOM); } Copy code

It needs to be processed once in the diff method. If the old virtual DOM exists, it will be compared. If it does not exist, the current virtual DOM will be directly placed in the container.

src/tinyReact/diff.js

import mountElement from './mountElement' ; function diff ( virtualDOM, container, oldDOM ) { //Determine whether oldDOM is patrolling if (!oldDOM) { return mountElement(virtualDOM, container); } } Copy code

It is necessary to determine whether the virtual DOM to be converted is a component or an ordinary label. Need to be processed separately, here we first default to only native jsx tags, hard-coded call mountNativeElement method.

src/tinyReact/mountElement.js

import mountNativeElement from './mountNativeElement' ; function mountElement ( virtualDOM, container ) { //Process native jsx and component jsx mountNativeElement(virtualDOM, container); } Copy code

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

src/tinyReact/mountNativeElement.js

import createDOMElement from './createDOMElement' ; function mountNativeElement ( virtualDOM, container ) { //Convert the virtual dom into a real object let newElement = createDOMElement(virtualDOM); //Put the converted DOM object on the page container.appendChild(newElement); } Copy code

The method of creating the real DOM is defined separately for easy reuse. Need to determine if it is an element node to create the corresponding element, if it is a text node, create the corresponding text. Then create child nodes recursively. Finally, put the node we created in the specified container container.

src/tinyReact/createDOMElement.js

import mountElement from "./mountElement" ; function createDOMElement ( virtualDOM ) { let newElement = null ; if (virtualDOM.type === 'text' ) { //Use createTextNode to create text node newElement = document .createTextNode(virtualDOM.props.textContent); } else { //Element node uses createElement to create newElement = document .createElement(virtualDOM.type); } //Recursively create child nodes virtualDOM.children.forEach( child => { mountElement(child, newElement); }) return newElement; } Copy code

Add attributes to real DOM objects

We know that the attributes are stored in the props of the virtual DOM. We only need to loop this attribute when creating the element and put these attributes in the real element.

When adding attributes, different situations need to be considered. For example, events and static attributes are different, and the method of adding attributes is also different, and the setting methods of Boolean attributes and value attributes are different. It is also necessary to determine whether the attribute is children, because children are not attributes, but child elements defined by ourselves. If the attribute is className, it needs to be converted to class and added.

src/tinyReact/createDOMElement.js

We define a separate method to add attributes to the element, call this method after creating the element, here is called updateNodeElement

import mountElement from "./mountElement" ; import updateNodeElement from "./updateNodeElement" ; function createDOMElement ( virtualDOM ) { let newElement = null ; if (virtualDOM.type === 'text' ) { //The text node uses createTextNode to create newElement = document .createTextNode(virtualDOM.props.textContent); } else { //Element node uses createElement to create newElement = document .createElement(virtualDOM.type); //Call the method of adding attributes updateNodeElement(newElement, virtualDOM) } //Recursively create child nodes virtualDOM.children.forEach( child => { mountElement(child, newElement); }) return newElement; } Copy code

1. you need to get the attribute list of the node object, use Object.keys to get the attribute name, and then use forEach to traverse.

src/tinyReact/updateNodeElement.js

If the property name starts with on, we consider it to be an event, and then we intercept the event name, that is, remove the on at the beginning and lowercase the string, and use addEventListener to bind the event.

If the attribute name is value or checked and cannot be set using setAttribute, the direct attribute name is equal to the attribute value.

Finally, if the attribute name is judged to be className, it will be converted to class. If it 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]; //Determine whether it is an event Property if (propName.startsWith( 'on' )) { //Cut out 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 by setAttribute, it can be set directly by attribute newElement[propName] = newPropsValue; } else if (propName !== 'children' ) { //exclude children if (propName === 'className' ) { newElement.setAttribute( 'class' , newPropsValue) } else { newElement.setAttribute(propName, newPropsValue) } } }) } Copy code

Component rendering-distinguish between functional components and class components

Before rendering the component, we must first make it clear that the virtual DOM type value of the component is a function, and this is true for both function components and class components.

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

Component's virtual DOM

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

When rendering components, we must first distinguish between Component and Native Element. If it is a Native Element, it can be rendered directly. We have already dealt with this before. If it is a component, special treatment 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

Then you 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 code

We deal with the component in the mountComponent method. First of all, we have to consider whether this component is a class component or a function component, because their processing methods are different, and whether there is a render function on the prototype can be used. We can use the isFunctionComponent function to judge

src/tinyReact/mountComponent.js

If the type exists, and the object is a function, and the render method does not exist on the object, it 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 code

src/tinyReact/isFunctionComponent.js

import isFunction from "./isFunction" ; function isFunctionComponent ( virtualDOM ) { const type = virtualDOM.type; return type && isFunction(virtualDOM) && !(type.prototype && type.prototype.render) } Copy code

Processing function component

Let's deal with the function component first. The function component is actually very simple. You only need to call the type function to get the returned virtual dom. After obtaining, we need to determine whether the newly obtained virtual DOM is a component, if it is to continue to call mountComponent, if not, call the mountNativeElement method directly for the native DOM element to render the virtual DOM to the page.

src/tinyReact/mountComponent.js

import isFunction from './isFunction' ; import isFunctionComponent from './isFunctionComponent' ; import mountNativeElement from './mountNativeElement' ; function mountComponent ( virtualDOM, container ) { //stored virtual DOM let nextVirtualDOM = null ; //determine whether the component is a class component or a function component if (isFunctionComponent(virtualDOM)) { //handle function component nextVirtualDOM = buildFunctionComponent(virtualDOM); } //Determine whether it is still a function component if (isFunction(nextVirtualDOM)) { mountComponent(nextVirtualDOM, container); } //Render nextVirtualDOM mountNativeElement(nextVirtualDOM, container); } function buildFunctionComponent ( virtualDOM ) { return virtualDOM.type(); } Copy code

The props attribute of the function component

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

const root = document .getElementById( 'root' ); function Head ( props ) { return < div > {props.title} </div > } TinyReact.render( < Head title = "hello"/> , root); Copy code

Based on our previous understanding, we know that there is a props parameter on the component, and this value can be obtained on the props inside the component. When we are going to render the function component, we can use the buildFunctionComponent method in this The component function is called in the method, and we can pass in props when calling. Here we need to be compatible with empty objects.

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

Class component rendering

Here we first create a class component, in React, the class component needs to inherit the Component class. We can create them all.

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 code

After the preparation work is completed, we need to render the class component, which is also implemented in mountComponent.js. We have implemented the rendering function component here before.

if (isFunctionComponent(virtualDOM)) { //handle function component nextVirtualDOM = buildFunctionComponent(virtualDOM); } else { //Processing class components } Copy code

We create a buildClassComponent method to process class components. This function receives the virtual DOM. In this function, we need to get the instance object of the component, because we can get the render method only by getting the instance object, and we can get the virtual output of the component by calling the render method. DOM object.

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

The rest of the logic is the same as that of the functional component, and it is judged whether the returned DOM is a component DOM or a native DOM. If it is a component DOM, continue to pass it recursively to mountComponent, if it is a native DOM, call mountNativeElement for rendering.

Class component props processing

We know that the passed parameters can be obtained through this.props in the class component. Our class component is integrated with the Component parent class. We can call the method of the parent class in the subclass to make the props in the parent class equal to the passed in props, so that subclasses can get props.

We add a constructor to the subclass, receive props, and then call the super parent class to pass the 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 code

Get props in the constructor of the parent class and assign them to the props attribute. In this way, the subclass inherits the parent class, and the subclass also has this attribute.

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

Finally, we can pass in the props when instantiating the component.

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

Update DOM element-text node

To update the DOM elements in the page, it is necessary to use the virtual DOM comparison. The new virtual DOM and the old virtual DOM must be compared to find the difference, and update the difference to the page to achieve the minimal update of the DOM. .

When comparing the virtual DOM, the updated virtual DOM and the virtual DOM before the update need to be used. The updated virtual DOM can currently be passed through the render method. The question now is how to obtain the virtual DOM before the update.

For the virtual DOM before the update, it actually corresponds to the real DOM that has been displayed on the page. In this case, when we create the real DOM object, we can add the virtual DOM to the properties of the real DOM object, and perform the virtual DOM Before the comparison, the corresponding virtual DOM object can be obtained through the real DOM object. In fact, it is obtained through the third parameter of the render method, container.firstChild.

1. we need 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 then add a _virtualDOM attribute to it to store the corresponding virtual DOM.

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

When we initially defined the render method, we actually passed three parameters, the current virtual DOM object, the container object to be rendered, and the old virtual DOM object.

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

In fact, the third parameter is not passed in by the render method, but is obtained from the page, which should be the container.firstChild object. That is, the content object rendered in the current container. Because we all know that the jsx code must have a package tag, which means that the container can only have one child element, so use firstChild.

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

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

const oldVirtualDOM = oldDOM && oldDOM._virtualDOM; duplicated code

Then we can go to compare. If oldVirtualDOM exists, we first compare the tag types of the two elements to be the same. If the two element types are the same, we need to determine whether it is a text type node or an element type node. The text type directly updates the content, and the element type needs to update the tag attributes.

import mountElement from './mountElement' ; import updateTextNode from './ updateTextNode ' ; function diff ( virtualDOM, container, oldDOM ) { //Get the old virtual DOM object const oldVirtualDOM = oldDOM && oldDOM._virtualDOM; //Determine whether oldDOM is patrolling if (!oldDOM) { return mountElement(virtualDOM, container); } else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) { //The two element types are the same, it is necessary to determine whether it is a text type node or an element type node //The text type directly updates the content //The element type needs to update the label The attribute if (virtualDOM.type === 'text' ) { //update content updateTextNode(virtualDOM, oldVirtualDOM, oldDOM); } else { //Update element attributes } //Traverse the child elements for comparison virtualDOM.children.forEach( ( child, i ) => { diff(child, oldDOM, oldDOM.childNodes[i]); }) } } Copy code

Here we also extract an update method. In this method, it is judged whether the content is the same, and if it is not the same, it is updated.

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 DOM oldDOM._virtualDOM = virtualDOM; } } Copy code

Update DOM element-node attributes

In fact, it is to compare the new and old node attribute objects to find the difference part, and then update the difference part to the node attribute. We use the previously defined updateNodeElement method here, and we used this method to update the attributes of the element before.

if (virtualDOM.type === 'text' ) { //update content updateTextNode(virtualDOM, oldVirtualDOM, oldDOM); } else { //Update element attributes //Which element to update, updated virtual DOM, old virtual DOM updateNodeElement(oldDOM, virtualDOM, oldVirtualDOM) } Copy code

Then we modify the updateNodeElement method and add the oldVirtualDOM parameter in it. Get the new and old node property objects newProps and oldProps. The oldVirtualDOM here does not always exist, and only exists when it is updated, so it needs to be compatible with the empty state.

When recycling a new attribute object, you can get the attribute name, and you can compare the two attribute values by comparing the attribute name with the old attribute value.

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 code

Compare whether the two values are the same, if they are not the same, do an update operation. In the update operation, the event needs to be paid attention to, and the original event should be cleared.

//If there is an original event, it needs to be deleted. if (oldPropsValue) { newElement.addEventListener(eventName, oldPropsValue); } Copy code

If an attribute is deleted, the attribute on the DOM object needs to be deleted. We can cycle oldProps, if there is no newProps, it is deleted.

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

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

const oldVirtualDOM = oldDOM && oldDOM._virtualDOM duplicated code

Determine whether oldVirtualDOM exists, if it exists, continue to determine whether the virtual DOM types to be compared are the same, if the types are the same, determine whether the node type is text, if it is a text node comparison, call the updateTextNode method, if it is an element node comparison, call the updateNodeElement method .

else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) { //The two element types are the same, it is necessary to determine whether it is a text type node or an element type node //The text type directly updates the content //The element type needs to update the label Attribute if (virtualDOM.type === 'text' ) { //update content updateTextNode(virtualDOM, oldVirtualDOM, oldDOM); } else { //Update element attributes //Which element to update, updated virtual DOM, old virtual DOM updateNodeElement(oldDOM, virtualDOM, oldVirtualDOM) } } Copy code

The comparisons are all top-level elements. After the comparison of the top-level elements is completed, the sub-elements need to be recursively compared.

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

If the two node types are not the same, how do we deal with it? 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 situation.

if (!oldDOM) { return mountElement(virtualDOM, container); } else if (virtualDOM.type !== oldVirtualDOM.type && typeof virtualDOM.type !== 'function' ) { //If the tag types are different and they are not components. const newElement = createDOMElement(virtualDOM); //Replace DOM element oldDOM.parentNode.replaceChild(newElement, oldDOM); } The else IF copy the code

Delete node

Deleting a node occurs after the node is updated, and it occurs on all child nodes under the same parent node. After the node is updated, if the number of old node objects is more than the number of new virtual DOM nodes, it means that there are nodes that need to be deleted .

Here we get the number of DOM nodes. If the number of new and old nodes is not the same, we loop the old DOM nodes, and then delete from the previous to the end, until the number of new and old DOM nodes is the same.

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

Because we have already updated the method to ensure that the corresponding Nodes are the same before, the redundant Node nodes can be deleted directly, and there is no need to worry about the problem of the remaining Node nodes being out of sync.

Status update of class components

To update the class component, you need to implement the setState method. We first define an Alert component. When the button is clicked, the title attribute in the component is updated and updated to the page.

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 Title </button > </div > } } Copy code

In fact, the setState we call here should be setState in the parent class Component. When a subclass calls setState, it must first be clear that this in setState points to an instance object of the subclass.

setState (state) { this .state = Object .assign({}, this .state, state); } Copy 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 get the latest virtual DOM through render, and then compare and update it with the old virtual DOM.

It is more troublesome to compare here. We can get the current virtual DOM by calling the render method, but we can't get the DOM displayed on the page. We can define a setDOM method to store the DOM displayed on the page. Pass it to setDOM when the class component is instantiated. Then call the diff method for comparison and update.

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 (); //achieve comparison diff(virtualDOM, container, oldDOM); } setDOM (dom) { //Store the DOM object displayed on the page this ._dom = dom; } getDOM () { //Get the DOM displayed on the page return this ._dom; } } Copy code

Component update function

When the component is updated, the same component may be rendered or different components may be rendered. We must take care of both.

First of all, we have to determine in the diff whether the virtual DOM to be updated is a component. If it is a component, it is judged whether the component to be updated and the component before the update are the same component. If it is not the same component, there is no need to perform component update operations. Call the mountElement method directly to add the virtual DOM returned by the component to the page.

If it is the same component, perform the update component operation, which is actually to pass the latest props to the component, then call the render method of the component to get the latest virtual DOM object returned by the component, and then pass the virtual DOM object to the diff method to let The diff method finds the difference and updates the difference to the real DOM object.

In the process of updating components, different life cycle functions of the device are called at different stages.

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

If the component is divided into multiple situations, add the diffComponent method for processing. This method receives four parameters, the first parameter is the virtual DOM object of the component itself, through which the latest props of the component can be obtained, and the second parameter is the instance object of the component to be updated, through which the life cycle of the component can be called Function, you can update the props property of the component, and you can get the latest virtual DOM object returned by the component. The third parameter is the DOM object to be updated. When updating the component, you need to modify the existing DOM object to achieve DOM Minimize the operation and get the old virtual DOM object. The fourth parameter is the container element to be updated.

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

In diffComponent, we have to judge whether virtualDOM and oldComponent are the same component, as long as they judge 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 on the page, that is, delete the original There is DOM, add new DOM mountElement(virtualDOM, container, oldDOM); } } function isSameComponent ( virtualDOM, oldComponent ) { //To determine whether they are the same component, just determine whether their constructors are the same to return oldComponent && virtualDOM.type === oldComponent.constructor; } Copy code

If it is not the same component, replace the original component. Need to receive oldDOM in mountNativeElement, and then delete this DOM.

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

If the component that needs to be updated is the same component as the old component, we use the updateComponent method to achieve it. 4.parameters are passed in: virtualDOM, container, oldDOM, and container.

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

The thing to do in this method is component update. 1. we need to update the props property in the component. The latest props are stored in the props of virtualDOM. We need to call a method to update props through the oldComponent instance and pass the props to him.

We need to define the method for updating props in the Comonent.js class, updateProps, to receive a props.

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

Next, we can update props by calling the updateProps method of oldComponent.

oldComponent.updateProps(virtualDOM.props); Copy code

After the update, we need to get the latest virtual DOM. Then compare and update through the diff algorithm. 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 separately and updated. diff(nextVirtualDOM, container, oldDOM) } Copy code

Component life cycle

In the process of component update, we also need to call the life cycle function of the component, we first add the life cycle in the Component class by default.

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

In the updateComponent function, we should call the componentWillReceviceProps life cycle first, and pass in the latest props when calling this life cycle.

oldComponent.componentWillReceviceProps(virtualDOM.props); Copy code

Then we have to call shouldComponentUpdate life cycle to determine whether 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 separately and updated. diff(nextVirtualDOM, container, oldDOM) } Copy code

The componentWillUpdate life cycle is then called.

//Life cycle oldComponent.componentWillUpdate(virtualDOM.props); Copy code

After the component update ends, the componentDidUpdate life cycle needs to be executed. The props passed in here should be the props before the update, and we can define a variable in advance to store it.

function updateComponent ( virtualDOM, oldComponent, oldDOM, container ) { //life cycle oldComponent.componentWillReceviceProps(virtualDOM.props); //Determine whether to update the life cycle if (oldComponent.shouldComponentUpdate(virtualDOM.props)) { //Store the props before the update let prevProps = oldComponent.props; //Life cycle 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 separately and updated. diff(nextVirtualDOM, container, oldDOM) //Life cycle oldComponent.componentDidUpdate(prevProps); } } Copy code

Realize ref function

Add the ref attribute to the node to get the DOM object of this node. For example, in Demo, add the ref attribute to the p tag, the purpose is to get the p element object, and get the content in 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 content </button > </div > } } TinyReact.render( < Demo name = "yindong"/> , root); Copy code

It is also relatively simple to implement. When creating a node, judge whether there is a ref attribute in its virtual DOM object. If so, call the method stored in the ref attribute and pass the created DOM object as a parameter to the ref method, so that the component is rendered You can get the element object and store the element object as a component attribute when it is a node. Add in the createDOMElement method.

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

The ref attribute can also be added to the class component, the purpose is to obtain the instance object of the component. In the mountComponent method, it can be judged that if the current processing is a class component, the instance object is obtained from the virtual DOM object returned by the class component, and ref is found in the props attribute of the instance object. If it exists, ref is called and the parameters are passed Just enter the instance object.

At the same time, here we can also add the componentDidMount life cycle function.

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

key attribute implementation

In React, when rendering type data, the key attribute is usually added to the rendered list element. The key attribute is the unique identifier of the data, helping React to identify which elements have been modified or deleted, so as to achieve the purpose of DOM minimization.

The key attribute does not need to be globally unique, but must be unique among sibling nodes under the same parent node. In other words, the key attribute is needed when comparing child nodes of the same type under the same parent node.

The implementation in our previous explanation of deleting nodes is to delete sequentially from back to front, keep the previous nodes the same, and delete the redundant nodes. In fact, this is very inefficient. The correct way is to find the unnecessary nodes and delete them directly. This effect can be achieved by using the key attribute.

When comparing two elements, if the types are the same, loop the child elements of the old DOM object to see if there is a key attribute on them. If there is, store the DOM object of this child element in a JavaScirpt object, and then loop The child element of the virtual DOM object to be rendered, obtain the key attribute of the child element in the loop process, and then use the key attribute to find the DOM object in the JavaScript object. If it can be found, it means that the element already exists. There is no need to re-render. If the element cannot be found through the key attribute, it means that the element is new.

Start to add this feature in the diff algorithm.

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

Cycle through the child elements of the virtual DOM to be rendered, get the key attribute of the child element, check whether the element exists, and if it exists, check whether the element at the current position is the element we expect. If not, insert it in this position.

//Loop the child elements of the virtual DOM to be rendered, 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 whether the element at the current position is the element we expect, if not, insert it at this position if (oldDOM.childNodes[i] && oldDOM.childNodes[i] !== domElement) { oldDOM.insertBefore(domElement, oldDOM.childNodes[i]); } } } }) Copy code

We also need to determine whether there are elements in keyedElements. If there is no element, then there is no key. Then we use the index to compare, and if there is a key, we use the key to compare.

let hasNoKey = Object .keys(keyedElements).length === 0 ; if (hasNoKey) { //Traverse the child elements for comparison virtualDOM.children.forEach( ( child, i ) => { diff(child, oldDOM, oldDOM.childNodes[i]); }) } else { //Loop the child elements of the virtual DOM to be rendered, 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 whether the element at the current position is the element we expect, if not, insert it at this position if (oldDOM.childNodes[i] && oldDOM.childNodes[i] != = domElement) { oldDOM.insertBefore(domElement, oldDOM.childNodes[i]); } } } }) } Copy code

Then we have to deal with the case that the DOM element cannot be found through the key attribute. If it is not found, it means that this is a new addition. We can directly render to the page through the mountElement method.

if (key) { const domElement = keyedElements[key]; if (domElement) { //Check whether the element at the current position is the element we expect, if not, insert it at this position if (oldDOM.childNodes[i] && oldDOM. childNodes[i] !== domElement) { oldDOM.insertBefore(domElement, oldDOM.childNodes[i]); } } } else { //New element mountElement(child, oldDOM, oldDOM.childNodes[i]) } Copy code

We need to judge in the mountNativeElement method. If oldDOM exists, we should insert it in front of oldDOM using the container.insertBefore method. If it does not exist, appendChild to the end.

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

Uninstall 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 means that a node has been deleted. Continue to determine whether there are elements in the keyedElements object. If not, use indexing to delete, if there is It is necessary to use the key attribute comparison method to delete.

The realization idea is to loop the old node. In the process of looping the old node, get the key attribute corresponding to the old node, and then find the old node in the new node according to the key attribute. If found, it means that the node has not been deleted. If it is not found, it means If the node is deleted, just call the method of uninstalling the node to delete the node.

When we delete the node in diff, we judge whether hasNoKey has a key.

//Delete nodes //Get old nodes const oldChildNodes = oldDOM.childNodes; //Determine the number of old nodes if (oldChildNodes.length> virtualDOM.children.length) { if (hasNoKey) { //Delete nodes in a loop for ( let i = oldChildNodes.length- 1 ; i> virtualDOM.children.length- 1 ; i--) { unmountNode(oldChildNodes[i]); } } else { //Delete the node through the key attribute //Take the old key to search for the new one, delete if you can t find it 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 code

Of course, uninstalling the node does not mean that you can delete the node directly. You also need to consider the following conditions. If the node to be deleted is a text node, you can delete it directly. If it is generated by a component, you need to call the uninstall lifecycle function of the component. The node contains nodes generated by other components and needs to call the uninstall life cycle of other components. If the node has a ref attribute, you need to delete the DOM node object passed to the component through the ref attribute. If there is an event, you also need to delete the event. We can handle these situations in the unmountNode function.

If it is a text node, delete it directly.

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

If it is not a text node, you need to determine whether the node is generated by a component, and if it is generated by a component, you need to call the uninstall life cycle of the component.

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

It is also necessary to determine whether there is a ref attribute on the node, and if there is one, it needs to be cleaned up

//Determine whether there is a ref attribute on the node, and if there is one, you need to clean up if (virtualDOM.props && virtualDOM.props.ref) { virtualDOM.props.ref( null ) } Copy code

It is also necessary to determine whether there is an event on the node, if there is a need to uninstall the event

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

Determine whether there are child nodes in the node. If there are, you need to delete them recursively, because the child nodes also need to determine the above content.

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

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

//delete node node.remove(); Copy code

So far we have finished deleting the DOM node.

function unmountNode ( node ) { //Get virtual DOM object const virtualDOM = node._virtualDOM; //delete the text node directly if (virtualDOM.type === 'text' ) { node.remove(); return ; } //Determine whether the node is generated by the component. const component = virtualDOM.component; if (component) { component.componentWillUnmount(); } //Determine whether there is a ref attribute on the node, and if there is one, you need to clean up if (virtualDOM.props && virtualDOM.props.ref) { virtualDOM.props.ref( null ) } //Determine whether the event exists Object .keys(virtualDOM.props).forEach( propsName => { if (propsName.startsWith( 'on' )) { const eventName = propsName.toLocaleLowerCase().slice( 0 , 2 ); const eventHandler = virtualDOM.props[propsName]; node.removeEventListener(eventName, eventHandler); } }) //Recursively delete child nodes if (node.childNodes.length> 0 ) { for ( let i = 0 ; i <node.childNodes.length; i++) { unmountNode(node.childNodes[i]); } } //delete node node.remove(); } Copy code