Virtual Dom and diff algorithm

Posted by uluru75 on Tue, 22 Feb 2022 10:24:46 +0100

1. What exactly is JSX

When you use react, you will write JSX. What is JSX? It is an extension of Javascript syntax. React uses it to describe what the user interface looks like. Although it looks very much like HTML, it is Javascript. Babel will compile JSX into the React API before the react code is executed

<div className="container">
  <h3>Hello React</h3>
  <p>React is great </p>
</div>
React.createElement(
  "div",
  {
    className: "container"
  },
  React.createElement("h3", null, "Hello React"),
  React.createElement("p", null, "React is great")
);

The above jsx code will be compiled into the following code before execution, and each node will be compiled into react Call of createElement method

React. The createElement method is used to create a Virtual Dom object

The flow of JSX syntax rendering page is: first, compile the Jsx syntax through Babel to compile the createElement method, then return the virtualdom object by calling the completion method, and finally convert the virtual dom to real dom by React, and render it to the page.

2. DOM operation problem

Using JavaScript to manipulate DOM is essential in modern web applications, but unfortunately it is much slower than most other JavaScript operations.

Most JavaScript # frameworks update DOM far more than they have to, making this slow operation worse.

For example, if you have a list of ten items and you only change the first item in the list, most JavaScript frameworks will rebuild the whole list, which is ten times more than necessary.

Low update efficiency has become a serious problem. In order to solve this problem, React popularizes something called Virtual dom. The purpose of Virtual DOM is to improve the efficiency of JavaScript operating DOM objects.

3. What is Virtual Dom

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

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

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

{
  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"
          }
        }
      ]
    }
  ]
}

4. How does Virtual Dom improve efficiency

Accurately find the changed DOM object and update only the changed part.

After "react" creates the DOM object for the first time, it will create its corresponding "Virtual DOM" object for each DOM object. Before the DOM object is updated, react "will update all the" Virtual DOM "objects first, and then" react "will compare the updated" Virtual DOM "with the" Virtual DOM "before the update to find out the changed part, React , will update the changed part to the real DOM object, and react , only updates the part that needs to be updated.

The update and comparison of Virtual DOM objects only occur in memory and will not render anything in the view, so the performance loss cost of this part is insignificant.

example:

<div id="container">
	<p>Hello React</p>
</div>



<div id="container">
	<p>Hello Angular</p>
</div>



const before = {
  type: "div",
  props: { id: "container" },
  children: [
    {
      type: "p",
      props: null,
      children: [
        { type: "text", props: { textContent: "Hello React" } }
      ]
    }
  ]
}



const after = {
  type: "div",
  props: { id: "container" },
  children: [
    {
      type: "p",
      props: null,
      children: [
        { type: "text", props: { textContent: "Hello Angular" } }
      ]
    }
  ]
}

5. Create Virtual Dom object

JSX , will be converted to , React , by , Babel , before the , React , code is executed When the createElement method is called, the element type, element attributes, and element child elements will be passed in. The return value of the createElement method is the built Virtual DOM object.

The following is the desired text node representation. An object is stored in children to represent text instead of in the form of a string

{
  type: "div",
  props: null,
  children: [{type: "text", props: {textContent: "Hello"}}]
}
/**
 * Create Virtual DOM
 * @param {string} type type
 * @param {object | null} props attribute
 * @param  {createElement[]} children Child element
 * @return {object} Virtual DOM
 */
function createElement (type, props, ...children) {
	return {
    type,
    props,
    children
  } 
}

From the third parameter of the 'createElement' method, they are all child elements. When defining the 'createElement' method, pass ` Children ` place all child elements in the children array.

Implement the createElement method:

export default function createElement(type, props, ...children) {
// Copy array for operation
// If it is a text node, it will not be processed if it is in the form of object. If it is in the form of string, it will be converted into the form of object with createElement
  const childElements = [].concat(...children).reduce((result, child) => {
// Remove the logic of null, true and false in the calling code to judge whether to display
    if (child !== false && child !== true && child !== null) {
      if (child instanceof Object) {
        result.push(child)
      } else {
        result.push(createElement("text", { textContent: child }))
      }
    }
    return result
  }, [])
  return {
    type,
    props: Object.assign({ children: childElements }, props),
    children: childElements
  }
}

6. Rendering Virtual Dom objects as Dom objects

The Virtual DOM object can be updated to a real DOM object by calling the render method.

Before updating, you need to determine whether there is an old Virtual dom. If there is a difference that needs to be compared, if there is no difference, you can directly convert the Virtual DOM into a DOM object.  

At present, we only consider the case that there is no old Virtual DOM first, that is, directly update the Virtual DOM object to the real DOM object first.

The general idea is as follows

// render.js
// The first parameter is the virtual dom object, the second parameter is the rendering container, and the third parameter represents the old dom in the page
export default function render(virtualDOM, container, oldDOM = container.firstChild) {
  // In the diff method, whether the comparison is needed or not is judged. The comparison or not is operated in the diff method  
  diff(virtualDOM, container, oldDOM)
}
// diff.js
import mountElement from "./mountElement"

export default function diff(virtualDOM, container, oldDOM) {
  // Determine whether oldDOM exists
  if (!oldDOM) {
    // If there is no need for comparison, directly convert the Virtual DOM to the real dom
    mountElement(virtualDOM, container)
  }
}

Before converting virtual DOM, you need to determine the class of virtual DOM: Component VS Native Element. (normal jsx elements)

Different types require different processing. If it is a "Native" Element, it needs to be converted directly.

If it is a component, you also need to get the component instance object, get the virtual DOM returned by the component through the component instance object, and then convert it.

At present, we only consider the case of , Native , Element , first

// mountElement.js
import mountNativeElement from "./mountNativeElement"

export default function mountElement(virtualDOM, container) {
  // Convert a Native Element by calling the mountnateelement method
  mountNativeElement(virtualDOM, container)
}
// mountNativeElement

import createDOMElement from "./createDOMElement"

export default function mountNativeElement(virtualDOM, container) {
  const newElement = createDOMElement(virtualDOM)
  container.appendChild(newElement)
}
// createDOMElement.js
import mountElement from "./mountElement"
import updateElementNode from "./updateElementNode"

export default function createDOMElement(virtualDOM) {
  let newElement = null
  if (virtualDOM.type === "text") {
    // Create text node
    newElement = document.createTextNode(virtualDOM.props.textContent)
  } else {
    // Create element node
    newElement = document.createElement(virtualDOM.type)
    // Update element attributes
    updateElementNode(newElement, virtualDOM)
  }
  // Render child nodes recursively
  virtualDOM.children.forEach(child => {
    // Because it is uncertain whether the child element is a NativeElement or a Component, the mountlelement method is called to determine
    mountElement(child, newElement)
  })
  return newElement
}

Summary: first, create and export a render method, which is a method opened to the outside of the framework for users of the framework to call. In the render method, the diff method is called. The diff method is used to determine whether the new and old DOM changes. It introduces three parameters to him, one is the virtual dom to be transformed, the other is the container to be mounted after transformation, and the third is the old DOM node. The diff method is used to judge whether the new and old DOM changes. In the diff method, we must first judge whether the old DOM node exists. If there is an old DOM node, we need to compare it. If it does not exist, we can directly convert the virtual dom into a real dom.

If there is no old DOM, call the mountlement method to convert this node. The reason for calling mountlement is that we are not sure whether the Virtual Dom is an ordinary Virtual Dom or a component Virtual Dom

If it is an ordinary Virtual Dom, we call the mountnatelement method to convert it into a real dom and display it in the page

In mountNativeElement, first create a newElement variable, and then judge the node type of Virtual Dom to judge whether to create a text node or an element node. Loop his child nodes, call the mounterelement method to process his child nodes, and finally render the page with appendChild

7. Add attributes to element nodes

// createDOMElement.js to create a dom node
// See whether the node type is text type or element type
if (virtualDOM.type === "text") {
  // Create text node and set node content
  newElement = document.createTextNode(virtualDOM.props.textContent)
} else {
  // Create a DOM element based on the value of the Virtual DOM type attribute
  newElement = document.createElement(virtualDOM.type)
  // Set properties for element
  updateElementNode(newElement, virtualDOM)
}
// updateNodeElement.js set properties

export default function updateElementNode(element, virtualDOM) {
  // Gets the property object in the VirtualDOM object to be parsed
  const newProps = virtualDOM.props
  // Put the attribute names in the attribute object into an array and loop the array
  Object.keys(newProps).forEach(propName => {
    const newPropsValue = newProps[propName]
    // Consider whether the attribute name starts with on. If yes, it means it is an event attribute onclick - > click
    if (propName.slice(0, 2) === "on") {
      const eventName = propName.toLowerCase().slice(2)
      element.addEventListener(eventName, newPropsValue)
      // If the attribute name is value or checked, it needs to be added in the form of []
    } else if (propName === "value" || propName === "checked") {
      element[propName] = newPropsValue
      // Remove children because it is a child element, not an attribute
    } else if (propName !== "children") {
      // The className attribute is handled separately and the class attribute is not added directly to the element because class is a keyword in JavaScript
      if (propName === "className") {
        element.setAttribute("class", newPropsValue)
      } else {
        // General properties
        element.setAttribute(propName, newPropsValue)
      }
    }
  })
}

Summarize the work done in adding attributes:

1. When we create a new element, we call the updatenodelement method to add attributes to the created element. The attribute we want to set is stored in the props attribute of the virtualDom object. Therefore, when calling this method, two values are passed: the first value is who to set the attribute for, and the second value is where the attribute to be set is stored.

2, in the called updateNodeElement method, we first get the attribute object to be set, then call Object.. The keys method stores the attributes of the object in an array, and then traverses the array. If the name of the attribute starts with on, we add an event to the element. If the attribute names of the element are "value" and "checked", we set this attribute directly for the element object. Then we need to remove children, because children is not an attribute, but a child element. Among the remaining attributes, we need to judge whether the attribute name is className. If so, we will add a class attribute (using setAttribute method). If not, it will prove to be an ordinary attribute. For an ordinary attribute, we will directly add its attribute name and attribute value using setAttribute method

8. Rendering components

8.1} function components

Before rendering a component, it should be clear that the virtual Dom type value of the component is a function, the types of function components and class components are functions, and the ordinary dom type is a string similar to span or div

// Original component
const Heart = () => <span>&hearts;</span>

// When quoting
<Heart />

// Virtual Dom of component
{
  type: f function() {},
  props: {}
  children: []
}

When rendering a Component, first distinguish the Component from the Native Element. If it is a Native Element, you can directly start rendering. If it is a Component, special treatment is required.

// mountElement.js
export default function mountElement(virtualDOM, container) {
  // Both class components and function components are essentially functions 
  // If the value of the type attribute of the Virtual DOM is a function, it means that the current Virtual DOM is a component
  if (isFunction(virtualDOM)) {
    // If it is a component, call the mountComponent method to render the component
    mountComponent(virtualDOM, container)
  } else {
    mountNativeElement(virtualDOM, container)
  }
}

// Is Virtual DOM a function type
export function isFunction(virtualDOM) {
  return virtualDOM && typeof virtualDOM.type === "function"
}

In the mountComponent method, distinguish between function components and types, and then handle them separately.

// mountComponent.js
import mountNativeElement from "./mountNativeElement"

export default function mountComponent(virtualDOM, container) {
  // Container for storing the Virtual DOM returned after component call
  let nextVirtualDOM = null
  // Distinguish between functional components and class components
  if (isFunctionalComponent(virtualDOM)) {
    // The function component calls the buildFunctionalComponent method to process the function component
    nextVirtualDOM = buildFunctionalComponent(virtualDOM)
  } else {
    // Class component
  }
  // Judge whether the obtained Virtual Dom is a component
  if (isFunction(nextVirtualDOM)) {
    // If it is a component, continue to call the mountComponent component
    mountComponent(nextVirtualDOM, container)
  } else {
    // If it's a Navtive Element, render it
    mountNativeElement(nextVirtualDOM, container)
  }
}

// Is Virtual DOM a functional component
// There are two conditions: 1 The value of type attribute of virtual DOM is function 2 A function cannot have a render method in its prototype object
// Only the prototype object of the class component has the render method 
export function isFunctionalComponent(virtualDOM) {
  const type = virtualDOM && virtualDOM.type
  return (
    type && isFunction(virtualDOM) && !(type.prototype && type.prototype.render)
  )
}

// Function component processing 
function buildFunctionalComponent(virtualDOM) {
  // Get the component function through the type attribute in the Virtual DOM and call
  // When calling the component function, pass the props attribute in the Virtual DOM object to the component function, so that the data can be obtained through the props attribute in the component
  // Component returns the Virtual DOM to render
  return virtualDOM && virtualDOM.type(virtualDOM.props || {})
}

Function component rendering summary:

1. In the mounterelement method, first judge whether the virtual Dom is a component, and judge whether it is a component based on whether the type of virtual Dom is a function. If it is a component, call the mountComponent method to render the component

2. In mountComponent, first judge whether the component is a functional component or a class component. Their judgment conditions are: the type of functional component Virtual Dom must be a function, and the render method cannot exist in the prototype method of the function. If the render method exists, it is a class component. (only class components have render methods.). Then the functional components are processed. His processing method is as follows: get the function of the component through the type attribute in the Virtual Dom and call it. After calling, his Virtual Dom element will be returned. When calling the function of the component, pass the props attribute in the Virtual Dom object to the component function, so that the data can be obtained through the props attribute in the component. After obtaining the component Virtual Dom to be rendered, continue to judge whether the obtained Virtual Dom type is a component. If so, continue to call the mountComponent method until the obtained Virtual Dom is not a component. At this time, call the mountnateelement method to render this ordinary jsx element, and finally complete the rendering of functional components.

Class 8.2 components

The class component itself is also a Virtual DOM. You can determine whether the component to be rendered is a class component or a function component through the value of the type attribute in the Virtual DOM.

After determining that the current component to be rendered is a class component, you need to instantiate the class component to obtain the class component instance object, and call 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 the Component parent class. Subclasses need to pass their props attribute to the Component parent class through the super method. The parent class will mount the props attribute as the parent class attribute. Subclasses inherit the parent class and naturally own the props attribute. The advantage of this is that when props is updated, the parent class can update the view according to the updated props help subclass.

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

// Class component
class Alert extends TinyReact.Component {
  constructor(props) {
    // If props inherits the parent and child classes of props, the data will be passed to the parent and child classes of props
    // Otherwise props is just an argument to the constructor function
    // The advantage of passing props to the parent class is that the parent class can help update props and update the component view when props changes
    super(props)
    this.state = {
      title: "default title"
    }
  }
  render() {
    return (
      <div>
        <h2>{this.state.title}</h2>
        <p>{this.props.message}</p>
      </div>
    )
  }
}
TinyReact.render(<Alert message="Hello React" />, root)

// Component.js parent class component implementation
export default class Component {
  constructor(props) {
    this.props = props
  }
}

// mountComponent.js
export default function mountComponent(virtualDOM, container) {
  let nextVirtualDOM = null
  // Distinguish between functional components and class components
  if (isFunctionalComponent(virtualDOM)) {
    // Function component
    nextVirtualDOM = buildFunctionalComponent(virtualDOM)
  } else {
    // Class component
    nextVirtualDOM = buildStatefulComponent(virtualDOM)
  }
  // Judge whether the obtained Virtual Dom is a component
  if (isFunction(nextVirtualDOM)) {
    mountComponent(nextVirtualDOM, container)
  } else {
    mountNativeElement(nextVirtualDOM, container)
  }
}

// Processing class components
function buildStatefulComponent(virtualDOM) {
  // Instantiate the class component to get the class component instance object and pass the props attribute into the class component
  const component = new virtualDOM.type(virtualDOM.props)
  // Call the render method in the class component to get the Virtual DOM to be rendered
  const nextVirtualDOM = component.render()
  // Returns the Virtual DOM to render
  return nextVirtualDOM
}



Class component rendering summary:

1. In the mountComponent method, after distinguishing the functional component and class component, get the virtual DOM to be rendered by the class component by calling the buildStateFulComponent method. The implementation idea of this method is to obtain the construction method of this class component first, and then declare a component construction instance, Call the render method to get its jsx element and get the virtual DOM to be processed in the next step

2. Pass props to the class component. In the class component, call the super method through the constructor construction method to pass the props of the component. (the super method in the subclass is the construction method used to trigger the parent class). Execute this in the construction method of the parent class Props = props, so there are props parameters in the parent class. At this time, the subclass inherits the parent class, so the subclass has props parameters. In the subclass, you can use this Props gets the value. The way to pass values to subclass constructor is to pass props in virtual DOM when creating an instance

9.Virtual Dom comparison

When comparing the Virtual DOM, we need to use the updated Virtual DOM and the Virtual DOM before the update. At present, we can transfer the updated Virtual DOM through the render method. The question now is how to obtain the Virtual DOM before the update?

For the Virtual DOM before updating, the corresponding is actually the real DOM object that has been displayed in the page. In this case, when we create a real DOM object, we can add the Virtual DOM to the properties of the real DOM object. Before Virtual DOM comparison, you can obtain the corresponding Virtual DOM object through the real DOM object. In fact, it is obtained through the third parameter of render method, container firstChild.

Container is actually the domain object we are attaching, which is a div with id of root. Understanding of firstchild: when writing jsx code, there is a principle that each section of jsx code must have a parent, and this firstchild points to this parent, so through container Firstchild can get the old dom elements in the page

<body>
    <div id="root"></div>
  </body>

When creating a real DOM object, add the corresponding Virtual DOM object to it

// createDomElement.js
import mountElement from "./mountElement"

export default function createDomElement(virtualDOM, container) {
  // Mount the Virtual DOM into the properties of the real DOM object to obtain its Virtual DOM during comparison
  newElement._virtualDOM = virtualDOM
}

9.1 the type of virtual DOM is the same

The Virtual DOM type is the same. If it is an element node, compare whether the attribute of the element node has changed. If it is a text node, compare whether the content of the text node has changed

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

// diff.js
// Get the Virtual DOM before updating
const oldVirtualDOM = oldDOM && oldDOM._virtualDOM

Judge whether the oldVirtualDOM exists. If it exists, continue to judge whether the Virtual DOM types to be compared are the same. If the types are the same, judge whether the node type is text. If it is text node comparison, call updateTextNode method. If it is element node comparison, call setAttributeForElement method

// diff.js
else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
  if (virtualDOM.type === "text") {
    // Text node compares whether the text content has changed
    updateTextNode(virtualDOM, oldVirtualDOM, oldDOM)
  } else {
    // Element nodes compare whether element attributes have changed
    setAttributeForElement(oldDOM, virtualDOM, oldVirtualDOM)
  }

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 will be updated. Since the real DOM object has changed, the latest Virtual DOM will be synchronized to the real DOM object.

function updateTextNode(virtualDOM, oldVirtualDOM, oldDOM) {
  // If the content of the text node is different
  if (virtualDOM.props.textContent !== oldVirtualDOM.props.textContent) {
    // Update content in real DOM objects
    oldDOM.textContent = virtualDOM.props.textContent
  }
  // Synchronize the Virtual DOM corresponding to the real DOM
  oldDOM._virtualDOM = virtualDOM

 

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

The idea is to obtain the props attribute in the updated and pre updated Virtual DOM respectively, cycle the props attribute in the new Virtual DOM, and check whether the attribute value in the new Virtual DOM has changed by comparison. If it has changed, the changed value needs to be updated to the real DOM object

Recycle the Virtual DOM object before updating, and check whether there are deleted attributes in the new Virtual DOM by comparison. If there are deleted attributes, you need to delete the corresponding attributes in the DOM object

// updateNodeElement.js
export default function updateNodeElement(
  newElement,
  virtualDOM,
  oldVirtualDOM = {}
) {
  // Get the attribute object corresponding to the node
  const newProps = virtualDOM.props || {}
  const oldProps = oldVirtualDOM.props || {}
  Object.keys(newProps).forEach(propName => {
    // Get property value
    const newPropsValue = newProps[propName]
    const oldPropsValue = oldProps[propName]
    if (newPropsValue !== oldPropsValue) {
      // Determine whether the attribute is an event attribute onclick - > Click
      if (propName.slice(0, 2) === "on") {
        // Event name
        const eventName = propName.toLowerCase().slice(2)
        // Add events to elements
        newElement.addEventListener(eventName, newPropsValue)
        // Delete the event handler function of the original event
        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, newPropsValue)
        }
      }
    }
  })
  // Judge whether the attribute is deleted
  Object.keys(oldProps).forEach(propName => {
    const newPropsValue = newProps[propName]
    const oldPropsValue = oldProps[propName]
    if (!newPropsValue) {
      // Property has been deleted
      if (propName.slice(0, 2) === "on") {
        const eventName = propName.toLowerCase().slice(2)
        newElement.removeEventListener(eventName, oldPropsValue)
      } else if (propName !== "children") {
        newElement.removeAttribute(propName)
      }
    }
  })
}

The above comparison is only for the top-level elements. After the comparison of the upper-level elements, it is also necessary to recursively compare the sub elements. In the sub elements, the first parameter virtualDom is the child element in the loop. The second DOM element to be rendered is actually the old olddom. The third parameter is the old real dom. Find the corresponding node in olddom according to the index

else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
    // Recursive comparison of child elements of Virtual DOM
    virtualDOM.children.forEach((child, i) => {
      diff(child, oldDOM, oldDOM.childNodes[i])
    })
  }

Comparison features of Virtual Dom: 1. Peer comparison is adopted. 2. Depth first comparison is adopted. It loops through its child nodes and then returns for peer comparison

 

9.2 compare different types of Virtual Dom

When the element node types to be compared are different, there is no need to continue the comparison. Directly use the new Virtual DOM to create the DOM object, and directly replace the old DOM object with the new DOM object. In the current situation, the components should be removed and treated separately.

// diff.js
else if (
  // If the Virtual DOM type is different
  virtualDOM.type !== oldVirtualDOM.type &&
  // And Virtual DOM is not a component, because the component needs to be processed separately
  typeof virtualDOM.type !== "function"
) {
  // Create real DOM elements from Virtual DOM
  const newDOMElement = createDOMElement(virtualDOM)
  // Replace the old DOM element with the created real DOM element. replaceChild can only be called through the parent
  oldDOM.parentNode.replaceChild(newDOMElement, oldDOM)
} 

9.3 deleting nodes

Deleting a node occurs after the node is updated and on all child nodes under the same parent node.

After the node update is completed, if the number of old node objects is more than the number of new VirtualDOM nodes, it indicates that there are nodes that need to be deleted.

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

9.4 component status update

The following code is to update the class components of the state. In the state object of the class component, there is a default title state. Click the change title button to invoke the handleChange method and call this. in the handleChange method. The setstate method changes the state value of the title.

class Alert extends TinyReact.Component {
  constructor(props) {
    super(props)
    this.state = {
      title: "default title"
    }
    // Change the point of this in the handleChange method so that this points to the class instance object
    this.handleChange = this.handleChange.bind(this)
  }
  handleChange() {
    // Call the setState method in the parent class to change the state
    this.setState({
      title: "changed title"
    })
  }
  render() {
    return (
      <div>
        <h2>{this.state.title}</h2>
        <p>{this.props.message}</p>
        <button onClick={this.handleChange}>change title</button>
      </div>
    )
  }
}

The setState method is defined in the parent class Component. The function of this method is to change the state of the subclass and generate a new state object.

// Component.js
export default class Component {
  constructor(props) {
    this.props = props
  }
  setState (state) {
    // The setState method is called by a subclass, where this points to the subclass instance object
    // So the state object of the subclass is changed
    this.state = Object.assign({}, this.state, state)
  }
}

Now the subclass can call the setState method of the parent class to change the state value. When the state object of the component changes, it is necessary to call the render method to update the component view.

Before updating the component, compare the updated Virtual DOM object with the non updated Virtual DOM to find out the updated part, so as to achieve the purpose of DOM minimization.

In the setState method, you can call this The render method obtains the updated Virtual DOM. Since the setState method is called by the subclass and this points to the subclass, the render method of the subclass is called here.

// Component.js
setState(state) {
  // The setState method is called by a subclass, where this points to the subclass
  // So the state of the subclass is changed
  this.state = Object.assign({}, this.state, state)
  // Get the latest Virtual DOM by calling the render method
  let virtualDOM = this.render()

To achieve comparison, we also need to obtain the Virtual DOM before updating. According to previous experience, we can obtain its corresponding Virtual DOM object from the DOM object. The DOM object before updating is actually the DOM object now displayed in the page. As long as we can obtain this DOM object, we can obtain its corresponding Virtual DOM object.

How to get the DOM object in the page? The DOM object in the page is mounted to the page through the mountNativeElement method, so we only need to call the method in the Component class in this method to save the DOM object in the Component class. When a subclass calls the setState method, it can call another method to obtain the DOM object in the setState method to obtain the previously saved DOM object.

// Component.js
// Method of saving DOM object
setDOM(dom) {
  this._dom = dom
}
// Method to get DOM object
getDOM() {
  return this._dom
}

Next, we need to study how to call setDOM method in the mountnateelement method. To call setDOM method, we must get the instance object of the class. Therefore, the current problem is how to get the instance object of the class in the mountnateelement method. This class does not refer to the Component class, Because we do not directly instantiate the Component class in the code, but instantiate its subclass. Since the subclass inherits the parent class, the setDOM method can also be called in the instance object of the subclass.

The mountNativeElement method receives the latest Virtual DOM object. If the Virtual DOM object is generated by a class component, the instance object of this class will be obtained first when generating the Virtual DOM object, and then the render method under the instance object will be called to obtain it. At that time, we can add the class component instance object to the properties of the Virtual DOM object, and the Virtual DOM object will eventually be passed to the mountnateelement method, so that we can obtain the component instance object in the mountnateelement method. If the class component instance object is obtained, we can call the setDOM method.

In the buildClassComponent method, add the component attribute for the Virtual DOM object, and the value is the instance object of the class component.

function buildClassComponent(virtualDOM) {
  const component = new virtualDOM.type(virtualDOM.props)
  const nextVirtualDOM = component.render()
  nextVirtualDOM.component = component
  return nextVirtualDOM
}

Obtain the component instance object in the mountnateelement method, and call the setDOM method to save the DOM object through the instance call, so as to obtain its Virtual DOM object through it during comparison

export default function mountNativeElement(virtualDOM, container) {
  // Get component instance object
  const component = virtualDOM.component
  // If the component instance object exists
  if (component) {
    // Save DOM object
    component.setDOM(newElement)
  }
}

Next, in the setState method, you can call the getDOM method to obtain the DOM object

setState(state) {
  this.state = Object.assign({}, this.state, state)
  let virtualDOM = this.render()
  // Get the DOM object being displayed in the page, through which you can get the Virtual DOM object of its object
  let oldDOM = this.getDOM()
}

Now the Virtual DOM object before the update and the Virtual DOM object after the update have been obtained. Next, we need to obtain the parent container object of the real DOM object, because it needs to be used when calling the diff method for comparison

setState(state) {
  this.state = Object.assign({}, this.state, state)
  let virtualDOM = this.render()
  let oldDOM = this.getDOM()
  // Gets the parent container object of the real DOM object
  let container = oldDOM.parentNode
}

Next, you can call the diff method for comparison. After comparison, the DOM object will be updated according to the logic we wrote before, and we can see the effect in the page

setState(state) {
    this.state = Object.assign({}, this.state, state)
    let virtualDOM = this.render()
    let oldDOM = this.getDOM()
    let container = oldDOM.parentNode
    // comparison
    diff(virtualDOM, container, oldDOM)
  }

9.5 component update

Judge whether the Virtual DOM to be updated is a component in the diff method.

If it is a component, then judge whether the component to be updated and the component before updating are the same component. If it is not the same component, there is no need to update the component. Directly call the mountlelement method to add the Virtual DOM returned by the component to the page.

If it is the same component, update the component. In fact, it is to pass the latest props to the component, call the render method of the component to obtain the latest Virtual DOM object returned by the component, and then pass the Virtual DOM object to the diff method to find the difference, so as to update the difference to the real DOM object.

In the process of updating components, different component life cycle functions should be called at different stages.

Judge whether the Virtual DOM to be updated is a component in the diff method. If it is a component, it can be divided into many cases. Add a diffComponent method for processing

else if (typeof virtualDOM.type === "function") {
  // Components to be updated
  // 1) The virtualDOM object of the component itself can obtain the latest props of the component through it
  // 2) The instance object of the component to be updated can call the lifecycle function of the component, update the props property of the component, and obtain the latest Virtual DOM returned by the component
  // 3) The DOM to be updated needs to be modified on the existing DOM object when updating the component, so as to realize the DOM minimization operation and obtain the old Virtual DOM object
  // 4) If the component to be updated is not the same as the old component, the Virtual DOM returned by the component should be directly displayed in the page. At this time, container should be used as the parent container
  diffComponent(virtualDOM, oldComponent, oldDOM, container)
}

In the diffComponent method, judge whether the component to be updated is the same component before updating

// diffComponent.js
export default function diffComponent(virtualDOM, oldComponent, oldDOM, container) {
  // To judge whether the component to be updated and the component not updated are the same component, you only need to determine whether they use the same constructor
  if (isSameComponent(virtualDOM, oldComponent)) {
    // Update components belonging to the same component  
  } else {
    // Not the same component directly displays the component content in the page
  }
}
// virtualDOM.type updated component constructor
// oldComponent. Component constructor before constructor update
// If they are equivalent, they are the same component
function isSameComponent(virtualDOM, oldComponent) {
  return oldComponent && virtualDOM.type === oldComponent.constructor
}

If it is not the same component, there is no need to update the component. The component content is directly displayed on the page to replace the original content

// diffComponent.js
else {
  // Not the same component directly displays the component content in the page
  // Here, a new parameter oldDOM is added to the mounterelement method 
  // The function is to delete the existing DOM object in the page before inserting the DOM object into the page, otherwise both the old DOM object and the new DOM object will be displayed in the page
  mountElement(virtualDOM, container, oldDOM)
}

Delete the old DOM object in the mountnateelement method

export default function mountNativeElement(virtualDOM, container, oldDOM) {
 // If the old DOM object exists, delete it
  if (oldDOM) {
    unmount(oldDOM)
  }
}
// unmount.js
export default function unmount(node) {
  node.remove()
}

If it is the same component, you need to update the component and call the component life cycle function

First, add the life cycle function in the Component class. If the subclass needs to be used, it can be directly overridden

// Component.js
export default class Component {
  // Life cycle function
  componentWillMount() {}
  componentDidMount() {}
  componentWillReceiveProps(nextProps) {}
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps != this.props || nextState != this.state
  }
  componentWillUpdate(nextProps, nextState) {}
  componentDidUpdate(prevProps, preState) {}
  componentWillUnmount() {}
}

The new updateComponent method is used to update component operations and is invoked after if is established.

// diffComponent.js
if (isSameComponent(virtualDOM, oldComponent)) {
  // Update components belonging to the same component
  updateComponent(virtualDOM, oldComponent, oldDOM, container)
}

In the updateComponent method, calling the component's lifecycle function, updating the component to get the latest Virtual DOM, and finally calling the diff method to update it.

import diff from "./diff"

export default function updateComponent(
  virtualDOM,
  oldComponent,
  oldDOM,
  container
) {
  // Life cycle function
  oldComponent.componentWillReceiveProps(virtualDOM.props)
  if (
    // Call shouldComponentUpdate lifecycle function to determine whether to perform update operation
    oldComponent.shouldComponentUpdate(virtualDOM.props)
  ) {
    // Save a copy of the props that have not been updated
    let prevProps = oldComponent.props
    // Life cycle function
    oldComponent.componentWillUpdate(virtualDOM.props)
    // Update the props property of the Component. The updateProps method is defined in the Component type
    oldComponent.updateProps(virtualDOM.props)
    // Because the props of the component has been updated, the render method is called to obtain the latest Virtual DOM
    const nextVirtualDOM = oldComponent.render()
    // Mount the component instance object to the Virtual DOM
    nextVirtualDOM.component = oldComponent
    // Call the diff method to update the view
    diff(nextVirtualDOM, container, oldDOM)
    // Life cycle function
    oldComponent.componentDidUpdate(prevProps)
  }
}
// Component.js
export default class Component {
  updateProps(props) {
    this.props = props
  }
}

10. ref attribute

Adding the ref attribute to a node can obtain the DOM object of the node. For example, in the DemoRef class, the ref attribute is added to the input element to obtain the input DOM element object and obtain the content entered by the user in the text box when clicking the button

class DemoRef extends TinyReact.Component {
  handle() {
    let value = this.input.value
    console.log(value)
  }
  render() {
    return (
      <div>
        <input type="text" ref={input => (this.input = input)} />
        <button onClick={this.handle.bind(this)}>Button</button>
      </div>
    )
  }
}

The implementation idea is to judge whether there is ref attribute in the Virtual DOM object when creating the node. If so, call the method stored in the ref attribute and pass the created DOM object to the ref method as a parameter, so that the element object can be obtained and stored as a component attribute when rendering the component node.

// createDOMElement.js
if (virtualDOM.props && virtualDOM.props.ref) {
  virtualDOM.props.ref(newElement)
}

The ref attribute can also be added to the class component to obtain the instance object of the component. For example, in the following code, the Alert component is rendered in the DemoRef component, and the ref attribute is added to the Alert component to obtain the instance object of the Alert component in the DemoRef component.

class DemoRef extends TinyReact.Component {
  handle() {
    let value = this.input.value
    console.log(value)
    console.log(this.alert)
  }
  componentDidMount() {
    console.log("componentDidMount")
  }
  render() {
    return (
      <div>
        <input type="text" ref={input => (this.input = input)} />
        <button onClick={this.handle.bind(this)}>Button</button>
        <Alert ref={alert => (this.alert = alert)} />
      </div>
    )
  }
}

The implementation idea is that in the mountComponent method, if it is judged that the current processing is a class component, obtain the component instance object from the Virtual DOM object returned by the class component, judge whether there is ref attribute in the props attribute in the component instance object, if so, call the ref method and pass the component instance object to the ref method.

// mountComponent.js
let component = null
  if (isFunctionalComponent(virtualDOM)) {}
	else {
    // Class component
    nextVirtualDOM = buildStatefulComponent(virtualDOM)
    // Get component instance object
    component = nextVirtualDOM.component
  }
	// If the component instance object exists
	if (component) {
   	// Judge whether there is props attribute on the component instance object and whether there is ref attribute in props attribute
    if (component.props && component.props.ref) {
      // Call the ref method and pass the component instance object
      component.props.ref(component)
    }
  }

The code goes here and handles the life cycle function completed by component mounting

// If the component instance object exists
if (component) {
  component.componentDidMount()
}

11. key attribute

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

The key attribute does not need to be globally unique, but it must be unique among sibling nodes under the same parent node.

That is, the key attribute needs to be used when comparing child nodes of the same type under the same parent node.

11.1 node comparison

The implementation idea is that when comparing the two elements, if the types are the same, cycle the sub elements of the old DOM object to see if there is a key attribute on it. If so, store the DOM object of this sub element in a JavaScript object, then cycle the sub elements of the Virtual DOM object to be rendered, and obtain the key attribute of this sub element during the cycle, 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 and does not need to be re rendered. If this element cannot be found through the key attribute, it means that this element is new and needs to be rendered.

// diff.js
else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
  // Put the element with the key attribute into the keyedElements object
  let keyedElements = {}
  for (let i = 0, len = oldDOM.childNodes.length; i < len; i++) {
    let domElement = oldDOM.childNodes[i]
// Only nodes with nodeType of 1 can have a key attribute
    if (domElement.nodeType === 1) {
      let key = domElement.getAttribute("key")
      if (key) {
        keyedElements[key] = domElement
      }
    }
  }
}
// diff.js
// See if any element with key attribute is found
let hasNoKey = Object.keys(keyedElements).length === 0

// If no element with the key attribute is found, it is compared according to the index
if (hasNoKey) {
  // Recursive comparison of child elements of Virtual DOM
  virtualDOM.children.forEach((child, i) => {
    diff(child, oldDOM, oldDOM.childNodes[i])
  })
} else {
  // Use the key attribute for element comparison
  virtualDOM.children.forEach((child, i) => {
    // Get the key attribute of the element to be compared
    let key = child.props.key
    // If the key attribute exists
    if (key) {
      // Find the corresponding DOM element in the existing DOM element object
      let domElement = keyedElements[key]
      // If an element is found, it indicates that the element already exists and does not need to be re rendered
      if (domElement) {
        // Although DOM elements do not need to be re rendered, it is not certain that the position of the element has not changed
        // So also look at the location of the element
        // Check whether the (i) child element and domElement corresponding to oldDOM are the same element. If not, the element position has changed
        if (oldDOM.childNodes[i] && oldDOM.childNodes[i] !== domElement) {
          // The element position has changed
          // Insert domElement in front of the current element position olddom ChildNodes [i] is the current location
          // domElement is put into the current location
          oldDOM.insertBefore(domElement, oldDOM.childNodes[i])
        }
      } else {
        mountElement(child, oldDOM, oldDOM.childNodes[i])
      }
    }
  })
}
// mountNativeElement.js
if (oldDOM) {
  container.insertBefore(newElement, oldDOM)
} else {
  // Place the converted DOM object in the page
  container.appendChild(newElement)
}

11.2 node unloading

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 some nodes have been deleted. Continue to judge whether there are elements in the keyedElements object. If not, delete them by index. If so, delete them by key attribute comparison.

The implementation idea is to cycle the old node. In the process of cycling the old node, obtain the key attribute corresponding to the old node, and then find the old node in the new node according to the key attribute. If it is found, it means that the node has not been deleted. If it is not found, it means that the node has been deleted. Call the method of unloading the node to unload the node.

// Get the number of nodes
let oldChildNodes = oldDOM.childNodes
// If the number of old nodes is greater than the length of the new node to render
if (oldChildNodes.length > virtualDOM.children.length) {
  if (hasNoKey) {
    for (
      let i = oldChildNodes.length - 1;
      i >= virtualDOM.children.length;
      i--
    ) {
      oldDOM.removeChild(oldChildNodes[i])
    }
  } else {
    for (let i = 0; i < oldChildNodes.length; i++) {
      let oldChild = oldChildNodes[i]
      let 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) {
        unmount(oldChild)
        i--
      }
    }
  }
}

Unloading a node does not mean deleting the node directly. The following situations need to be considered

1. If the node to be deleted is a text node, you can delete it directly

2. If the node to be deleted is generated by the component, you need to call the component unloading life cycle function

3. If the node to be deleted contains nodes generated by other components, you need to call the uninstall life cycle function of other components

4. If the node to be deleted has a ref attribute, you need to delete the DOM node object passed to the component through the ref attribute

5. If there is an event on the node to be deleted, you need to delete the event handling function corresponding to the event

export default function unmount(dom) {
  // Get the virtual DOM object corresponding to the node
  const virtualDOM = dom._virtualDOM
  // Text if you want to delete the node
  if (virtualDOM.type === "text") {
    // Delete node directly
    dom.remove()
    // Prevent program from running down
    return
  }
  // Check whether the node is generated by the component
  let component = virtualDOM.component
  // If generated by component
  if (component) {
    // Call the component unload lifecycle function
    component.componentWillUnmount()
  }
  
  // If the node has a ref attribute, delete the DOM object passed to the component by calling the ref method again
  if (virtualDOM.props && virtualDOM.props.ref) {
    virtualDOM.props.ref(null)
  }

  // event processing 
  Object.keys(virtualDOM.props).forEach(propName => {
    if (propName.slice(0, 2) === "on") {
      const eventName = propName.toLowerCase().slice(2)
      const eventHandler = virtualDOM.props[propName]
      dom.removeEventListener(eventName, eventHandler)
    }
  })
	
  // Recursively delete child nodes
  if (dom.childNodes.length > 0) {
    for (let i = 0; i < dom.childNodes.length; i++) {
      unmount(dom.childNodes[i])
      i--
    }
  }
  	
  dom.remove()
}

Topics: Javascript React Algorithm