Small knowledge, big challenge! This paper is participating in theEssentials for programmers”Creative activities.

The army can seize the general also, every man cannot seize the will also. Zi Han, Analects of Confucius

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 what type the DOM object is. What properties does it have, what child elements does it have.

A Virtual DOM object can be thought of as a copy of a DOM object, but it cannot be displayed directly on the screen.


<div className="container">
    <h3>Hello React</h3>
    <p>React is great </p>
</div>

Copy the code
{
    type: "div".props: { className: "container" },
    children: [{type: "h3".props: null.children: [{type: "text".props: {
                        textContent: "Hello React"}}]}, {type: "p".props: null.children: [{type: "text".props: {
                        textContent: "React is great"}}]}Copy the code

How does Virtual DOM improve efficiency

Pinpoint the DOM objects that have changed and update only the parts that have changed.

After React creates a DOM object for the first time, it creates a Virtual DOM object for each DOM object. React updates all Virtual DOM objects before DOM objects are updated. React then compares the updated Virtual DOM to the pre-updated Virtual DOM to find out what has changed. React updates the changed part to the real DOM object. React updates only the necessary part.

Updates and comparisons of Virtual DOM objects occur only in memory and do not render anything in the view, so the performance cost for this part is negligible.

<div id="container">
    <p>Hello React</p>
</div>
Copy the code
const before = {
    type: "div",
    props: { id: "container" },
    children: [
        {
            type: "p",
            props: null,
            children: [
                { type: "text", props: { textContent: "Hello React" } }
            ]
        }
    ]
}
Copy the code

Create a Virtual DOM

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.

{
    type: "div",
    props: null,
    children: [{type: "text", props: {textContent: "Hello"}}]
}
Copy the code
/ * * * create Virtual DOM * @ param * @ {string} type type param {object | null} props attribute * @ param {createElement method []} children child elements  * @return {object} Virtual DOM */ function createElement (type, props, ... children) { return { type, props, children } }Copy the code

All child elements begin with the third argument to the createElement method. Children puts all the children elements into the children array.

Const virtualDOM = (<div className="container"> <h1> hello Tiny React</h1> <h2> </div> <h3> This will be changed) < / h3 > {= = 1 & 2 & < div > if 2 and 1 is equal to render the current content < / div >} {2 = = 2 && < div > < / div > 2} < span > this is a paragraph < / span > < button onClick = {() = > </button> <h3> this will be deleted </h3> 2, 3 </div>) console.log(virtualDOM)Copy the code

The expectation is that the text node should look like this

children: [
    {
        type: "text",
        props: {
            textContent: "React is great"
        }
    }
]
Copy the code

The following code is used to reconstruct the Virtual DOM.

// Make a copy of the original children and do not operate on the original array
constchildElements = [].concat(... children).map(child= > {
    // Determine if child is an object type
    if (child instanceof Object) {
        // Do not need to do anything directly to return
        return child
    } else {
        // Call the createElement method manually to convert the text to a Virtual DOM if it is not an object or text
        return createElement("text", { textContent: child })
    }
})
return {
    type,
    props,
    children: childElements
}
Copy the code

By observing the returned Virtual DOM, the text node has been converted to the Virtual DOM of object type, but booleans are also converted as text nodes. In JSX, if the Virtual DOM is converted to Boolean or NULL, Should not be updated into the real DOM, so the next thing to do is clear the Virtual DOM of booleans and NULL.

Const childElements = [].concat(...) const childElements = [].concat(... Children). Reduce ((result, child) => {// Determine the type of the child element by eliminating null true false if (child! = null && child ! = false && child ! = true) { if (child instanceof Object) { result.push(child) } else { result.push(createElement("text", { textContent: Child}))}} return result}, [])Copy the code

In the React component, you can get the child elements through props. Children, so you also need to store the child elements in the props object.

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

Render a Virtual DOM object as a DOM object

You can update a Virtual DOM object to a real DOM object by calling the Render method.

Before the update, it is necessary to determine whether there is an old Virtual DOM. If there is, it is necessary to compare the differences. If not, the Virtual DOM can be directly converted into a DOM object.

For now, we only consider the case where the old Virtual DOM does not exist, that is, we update the Virtual DOM object directly to the real DOM object.

// render.js export default function render(virtualDOM, container, OldDOM = container. FirstChild) {diff(virtualDOM, container, oldDOM)}Copy the code
// diff.js import mountElement from "./mountElement" export default function diff(virtualDOM, container, OldDOM) {// Check whether oldDOM exists if (! OldDOM) {// Convert the Virtual DOM to real DOM mountElement(virtualDOM, container)}}Copy the code

The Component VS Native Element class of the Virtual DOM needs to be determined before the virtual DOM transformation.

Different types need to do different processing if it is a Native Element directly converted.

If it’s a component, you also need to get the component instance object and use the component instance object to get the virtual DOM that the component returns and then convert it.

For now, only the Native Element is considered.

// mountElement.js import mountNativeElement from "./mountNativeElement" export default function MountElement (virtualDOM, container) {// Convert NativeElement mountNativeElement(virtualDOM, container) by calling mountNativeElement. container) }Copy the code
// mountNativeElement.js
import createDOMElement from "./createDOMElement"
export default function mountNativeElement(virtualDOM, container) {
    const newElement = createDOMElement(virtualDOM)
    container.appendChild(newElement)
}
Copy the code
// createDOMElement.js import mountElement from "./mountElement" import updateElementNode from "./updateElementNode" Export default function createDOMElement(virtualDOM) {let newElement = null if (virtualdom.type === "text") {// Create a text node NewElement = document. CreateTextNode (virtualDOM. Props. TextContent)} else {newElement = / / create the element node Document.createelement (virtualdom.type) // Update element attribute updateElementNode(newElement, VirtualDOM)} / / recursive rendering node virtualDOM children. ForEach (child = > {/ / because uncertain elements of the stator is NativeElement or Component invocation MountElement (child, newElement)}) return newElement}Copy the code

Add attributes to element nodes

If (virtualdom.type === "text") {// Create a text node and set the content of the node newElement = Document. CreateTextNode (virtualDOM. Props. TextContent)} else {/ / based on Virtual DOM newElement = the type attribute value to create DOM elements Document.createelement (virtualdom.type) // Set the attribute for the element updateElementNode(newElement, virtualDOM)}Copy the code
export default function updateElementNode(element, VirtualDOM) {// Get the property object in the virtualDOM object to be parsed const newProps = virtualdom.props // Put the property names in the property object into an array and loop through the array Object. Keys (newProps).foreach (propName => {const newPropsValue = newProps[propName] // Whether the property name starts with on is an event property onClick -> click if (propName.slice(0, 2) === "on") { const eventName = propName.toLowerCase().slice(2) element.addEventListener(eventName, NewPropsValue) / / if the property name is the value or checked to add in the form of []} else if (propName = = = "value" | | propName = = = "checked") { Element [propName] = newPropsValue // Remove children because it is a child and not an attribute} else if (propName! If (propName === "className") {if (propName === "children") {if (propName === "className") { Element.setattribute ("class", newPropsValue)} else {// Common element.setAttribute(propName, newPropsValue)}})}Copy the code

Rendering component

Function component

The first thing to make clear before rendering a component is that its Virtual DOM type value is a function, and this is true for both function components and class components.

// Original component const Heart = () => <span>&hearts; </span>Copy the code
<Heart />
Copy the code
// Component Virtual DOM {type: f function() {}, props: {} children: []}Copy the code

When rendering components, first distinguish Component from Native Element. If it is a Native Element, you can directly start rendering; if it is a Component, special processing.

// mountElement.js export default function mountElement(virtualDOM, Container) {// Both class components and function components are essentially functions. // If the Virtual DOM type attribute is a function, the current Virtual DOM is the component if (isFunction(virtualDOM)) {// If the component calls the mountComponent method to render the component mountComponent(virtualDOM, container) } else { mountNativeElement(virtualDOM, Container)}} // Whether Virtual DOM is a function type export function isFunction(virtualDOM) {return virtualDOM && typeof virtualDOM.type === "function" }Copy the code

The mountComponent method separates function components and types and handles them separately.

// mountComponent.js import mountNativeElement from "./mountNativeElement" export default function mountComponent(virtualDOM, Let nextVirtualDOM = null // Distinguish between functional components and class components if (isFunctionalComponent(virtualDOM)) {// Function components call the buildFunctionalComponent method to handle function components nextVirtualDOM = If (isFunction(nextVirtualDOM)) {// buildFunctionalComponent(virtualDOM)} else {// buildFunctionalComponent(virtualDOM) If it is a component, continue calling mountComponent to dissect mountComponent(nextVirtualDOM, Container)} else {// If it is a Navtive Element render mountNativeElement(nextVirtualDOM, Container)}} // Whether the Virtual DOM is a functional component // There are two conditions: 1. The type attribute of the Virtual DOM is a function. 2. Export function isFunctionalComponent(virtualDOM) {const type = virtualDOM && virtualDOM.type return ( type && isFunction(virtualDOM) && ! (type.prototype && type.prototype.render))} function buildFunctionalComponent(virtualDOM) When the component function is called, pass the props property of the Virtual DOM object to the component function so that the component can get data from the props property return virtualDOM && virtualDOM.type(virtualDOM.props || {}) }Copy the code

Class components

The class component itself is also a Virtual DOM, and you can determine whether the component to be rendered is a class component or a function component using the type attribute value in the Virtual DOM.

After determining the current component to render as a class component, it is necessary to instantiate the class component to obtain the class component instance object, and invoke the Render method in the class component through the class component instance object to obtain the Virtual DOM to be rendered by the component.

Class components need to inherit from the Component parent class. The subclass needs to pass its props attribute to the Component parent class using the super method. The parent class mounts the props attribute as its parent class. The advantage of this is that when the props is updated, the parent class can help the subclass update the view based on the updated props.

Suppose the following code is the class component we want to render:

Class Alert extends TinyReact.Com constructor(props) {constructor(props) {// If the props subclass extends TinyReact.Com, props (props) {// If the props subclass extends TinyReact.Com, props (props) {// If the props subclass extends TinyReact.Com, props (props) {// If the props subclass extends TinyReact.Com Props is just an argument to the constructor function. // The advantage of passing props to the parent class is that the parent class helps update the props update the component view when the props changes. title: "default title" } } render() { return ( <div> <h2>{this.state.title}</h2> <p>{this.props.message}</p> </div> ) } } TinyReact.render(<Alert message="Hello React" />, root)Copy the code
// component.js implements export default class Component {constructor(props) {this.props = props}}Copy the code

The mountComponent method calls buildStatefulComponent to get the Virtual DOM that the class component will render

// mountComponent.js export default function mountComponent(virtualDOM, Container) {let nextVirtualDOM = null // Distinguishes between functional components and class components if (isFunctionalComponent(virtualDOM)) {// Function component nextVirtualDOM = BuildFunctionalComponent (virtualDOM)} else {// nextVirtualDOM = buildStatefulComponent(virtualDOM) If (isFunction(nextVirtualDOM)) {mountComponent(nextVirtualDOM, container) } else { mountNativeElement(nextVirtualDOM, Function buildStatefulComponent(virtualDOM) {// Instantiate the class component to get the class component instance object and pass the props property to the class component const Component = new virtualdom.type (virtualdom.props) // Call the render method in the class component to get the VirtualDOM to render const nextVirtualDOM = Component.render () // Return nextVirtualDOM}Copy the code

Virtual DOM than

When making Virtual DOM comparison, the updated Virtual DOM and the pre-updated Virtual DOM need to be used. The updated Virtual DOM can be transferred through the render method at present. The question now is how to obtain the Virtual DOM before the update.

For the Virtual DOM before the update, the corresponding object is actually the actual DOM object that has been displayed on the page. Since this is the case, we can add the Virtual DOM to the properties of the real DOM object when we create it. Before the Virtual DOM comparison is performed, the corresponding Virtual DOM object can be obtained from the real DOM object, which is actually obtained from the third parameter of the Render method, container.firstChild.

Add the corresponding Virtual DOM object when you create the real DOM object

// mountElement.js import mountElement from "./mountElement" export default function mountNativeElement(virtualDOM, Container) {// Mount the Virtual DOM to the properties of the real DOM object to get its Virtual DOM newElement._virtualDOM = virtualDOM}Copy the code

The Virtual DOM type is the same

Virtual DOM has the same type. If it is an element node, it compares whether the attribute of the element node changes; if it is a text node, it compares whether the content of the text node changes

To achieve comparison, we need to obtain the corresponding Virtual DOM object from the existing DOM object.

// diff.js // Get the unupdated VirtualDOM const oldVirtualDOM = oldDOM && olddom._virtualdomCopy the code

Check whether oldVirtualDOM exists. If so, continue to check whether the VirtualDOM type to be compared is the same. If the VirtualDOM type is the same, check whether the node type is text. SetAttributeForElement is called if it’s an element node comparison

// diff.js else if (oldVirtualDOM && virtualdom.type === oldVirtualdom.type) {if (virtualdom.type === "text") {// Text node UpdateTextNode (virtualDOM, oldVirtualDOM, OldDOM)} else {// Whether an element node changes its attributes setAttributeForElement(oldDOM, virtualDOM, oldVirtualDOM)}Copy the code

The updateTextNode method is used to compare whether the content of the text node has changed. If so, the content in the real DOM object is updated. Since the real DOM object has changed, the latest Virtual DOM is synchronized to the real DOM object.

The function updateTextNode (virtualDOM oldVirtualDOM, oldDOM) {/ / if the text node content is different if (virtualDOM. Props. TextContent! = = oldVirtualDOM. Props. TextContent) {/ / update the contents of the real DOM object oldDOM textContent = virtualDOM. Props. TextContent} / real DOM/synchronization VirtualDOM olddom._virtualdom = virtualDOM}Copy the code

The setAttributeForElement method is used to set/update the element node attributes

The method is to obtain the props properties of the Virtual DOM after the update and the props properties of the Virtual DOM before the update, and then loop through the properties of the new Virtual DOM to check whether the properties of the new Virtual DOM have changed. If something changes, you need to update the changed value to the real DOM object

Recycle the Virtual DOM object before the update and check whether there are deleted attributes in the new Virtual DOM by comparison. If there are deleted attributes, the corresponding attributes in the DOM object should also be deleted

// updateNodeElement.js export default function updateNodeElement( newElement, virtualDOM, OldVirtualDOM = {}) {/ / get the corresponding property object const newProps = virtualDOM. Props | | {} const oldProps = oldVirtualDOM. Props | | {} Object. Keys (newProps). ForEach (propName => {// Get the property const newPropsValue = newProps[propName] const oldPropsValue = oldProps[propName] if (newPropsValue ! OnClick -> click if (propname. slice(0, 2) === "on") {// eventName const eventName = propName.tolowerCase ().slice(2) // Add event to element newElement.addEventListener(eventName, NewPropsValue) / / remove the existing event handler if (oldPropsValue) {newElement. RemoveEventListener (eventName, oldPropsValue) } } else if (propName === "value" || propName === "checked") { newElement[propName] = newPropsValue } else if (propName ! == "children") { if (propName === "className") { newElement.setAttribute("class", newPropsValue) } else { newElement.setAttribute(propName, ForEach (propName => {const newPropsValue =)}}}}) // Determine if the property is deleted object.keys (oldProps) newProps[propName] const oldPropsValue = oldProps[propName] if (! NewPropsValue) {// Property removed if (propname.slice (0, 2) === "on") { const eventName = propName.toLowerCase().slice(2) newElement.removeEventListener(eventName, oldPropsValue) } else if (propName ! == "children") { newElement.removeAttribute(propName) } } }) }Copy the code

The above comparison is only for the topmost element. After the comparison of the topmost element is completed, we also need to recursively compare the child elements

Else if (oldVirtualDOM && Virtualdom.type === oldVirtualdom.type) {// Compare the child elements of the VirtualDOM recursively virtualDOM.children.forEach((child, i) => { diff(child, oldDOM, oldDOM.childNodes[i]) }) }Copy the code

The Virtual DOM type is different

When the element node type of comparison is different, there is no need to continue the comparison. The new Virtual DOM is directly used to create the DOM object, and the new DOM object is directly used to replace the old DOM object. In this case, the component is stripped and processed separately.

// diff.js else if (// If the Virtual DOM type is different virtualdom.type! == oldVirtualdom.type && // And the VirtualDOM is not a component because the component handles typeof virtualdom.type separately! Const newDOMElement = createDOMElement(virtualDOM) // Use the created real DOM element Replace the old DOM elements oldDOM. ParentNode. ReplaceChild (newDOMElement oldDOM)}Copy the code

Remove nodes

Deleting a node occurs after a node is updated and occurs to all children of the same parent node.

If the number of old node objects exceeds the number of new VirtualDOM nodes after a node update is complete, a node needs to be deleted.

// Let oldChildNodes = olddom.childNodes // If the number of old nodes is greater than the length of new nodes to render if (oldChildNodes. Length > virtualDOM.children.length) { for ( let i = oldChildNodes.length - 1; i > virtualDOM.children.length - 1; i-- ) { oldDOM.removeChild(oldChildNodes[i]) } }Copy the code