What is VirtualDOM

A Virtual DOM object is a JavaScript representation of a DOM object, which uses JavaScript objects to describe DOM object information

<div className="app">
  <h1>Hello</h1>
  <p>Hello,world</p>
</div>
Copy the code

The above code is converted to JSX code

{
    type: 'div'.props: {
        className: 'app'
    },
    children: [{
        type: 'h1'.props: null.children: [{
            type: 'text'.props: {
                textContent: 'Hello'}}, {type: 'p'.props: null.children: [{
            type: 'text'.props: {
                textContent: 'Hello,world'}}}}]]Copy the code

Environment set up

npm init -y
Install dependency on WebPack-related and Babel
npm i -D webpack webpack-dev-server webpack-cli @babel/core @babel/preset-env @babel/preset-react babel-loader html-webpack-plugin
Copy the code

Configuration webpack

const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')

/ * *@type {import ('webpack').Configuration} * /
module.exports = {
    mode: 'development'.entry: './src/main.js'.output: {
        filename: 'main.bundle.js'.path: path.resolve(__dirname, 'dist')},module: {
        rules: [{
            test: /\.js(x?) $/,
            loader: 'babel-loader'}},devServer: {
        contentBase: path.join(__dirname, 'dist'),
        compress: true.port: 8080.hot: true,},devtool: 'source-map'.plugins: [
        new HtmlWebpackPlugin({
            title: 'Simple react'.template: './public/index.html'
        }),
        new webpack.HotModuleReplacementPlugin({})
    ]
}
Copy the code

Create webpack entry files SRC /main.js, public/index.html

Configuration for Babel, of which the pragma is compiled JSX expression is used to change the function used for the default is the React. The createElement method, which we can view the www.babeljs.cn/docs/babel-…

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

Here we just need to configure it to the name of the function we want

The development environment is set up

Create a VirtualDOM object

Create SpReact/index.js and SpReact/createElement.js

SpReact/index.js is used to export all methods on SpReact

import createElement from './createElement'

export default {
    createElement
}
Copy the code

SpReact. CreateElement is used to create VirtualDOM objects, which I have replaced with compiling JSX expressions

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

With that in mind, a simple VirtualDOM object has been created, and the method will be refined over time

Print the converted VirtualDOM object in main.js

import SpReact from "./SpReact"

const jsx = (<div><h1>VirtualDOM</h1><div>Create VirtualDOM</div></div>)

console.log(jsx)
Copy the code

JSX after transformation

{
    "type": "div"."props": null."children": [{
        "type": "h1"."props": null."children": ["VirtualDOM"] {},"type": "div"."props": null."children": ["Create VirtualDOM"]]}}Copy the code

We need to work on that

  • Text nodes are converted to objects for the corresponding virtualDOM
  • Booleans and NULL are not displayed on the page
  • Children can be accessed in props
export default function createElement(type, props, ... children) {
    const newChildren = children.reduce((res, child) = > {
        if (typeof child === 'string') {
            res.push(createElement('text', {
                textContent: child
            }))
        } else if (typeofchild ! = ='boolean'&& child ! = =null) {
            res.push(child)
        }
        return res
    }, [])
    return {
        type,
        props: {... props,children: newChildren
        },
        children: newChildren
    }
}
Copy the code

VirtualDOM objects are converted to real DOM objects

To create the Render method, we call the SpReact. Render method to render VirtualDOM onto the page

import diff from "./diff";

export default function render(VirtualDOM, container, oldDOM = container.lastChild) {
    diff(VirtualDOM, container, oldDOM)
}
Copy the code
export default function diff(virtualDOM, container, oldDOM) {
    if(! oldDOM) { mountElement(virtualDOM, container) } }Copy the code
// Add the created DOM node to the page
function mountElement(virtualDOM, container) {
    // Create the current virtualDOM DOM node, iterate over the create child elements, and call mountElement
    const dom = createDOM(virtualDOM)
    // Add the created DOM node to the page
    container.appendChild(dom)
}

// Create the current virtualDOM DOM node, traversing the DOM node where the child element was created
function createDOM(virtualDOM) {
    const type = virtualDOM.type
    let el
    if (type === 'text') {
        el = document.createTextNode(virtualDOM.props.textContent)
    } else {
        el = document.createElement(type)
    }
    virtualDOM.children.forEach(child= > {
        mountElement(child, el)
    })
    return el
}
Copy the code

Add attributes to DOM elements

const jsx = (<div className="aa"><h1 id="title" onClick={fn}>VirtualDOM</h1><div>Create VirtualDOM</div></div>)
Copy the code

We need to add events to the element node

function createDOM(virtualDOM) {
    const type = virtualDOM.type
    let el
    if (type === 'text') {
        el = document.createTextNode(virtualDOM.props.textContent)
    } else {
        el = document.createElement(type)
        // Bind events
        updateNodeElement(el,virtualDOM)
    }
    virtualDOM.children.forEach(child= > {
        mountElement(child, el)
    })
    return el
}
Copy the code

Add properties on props (except children) to the DOM

export default function updateNodeElement(el, virtualDOM) {
    const props = virtualDOM.props
    Object.keys(virtualDOM.props).forEach(propName= > {
        const val = props[propName]
        if (propName.startsWith('on')) {
            const eventName = propName.slice(2).toLowerCase()
            el.addEventListener(eventName, val)
        } else if (propName === 'className') {
            el.setAttribute('class', val)
        } else if(propName ! = ='children') {
            el.setAttribute(propName, val)
        }
    })
}
Copy the code

component

Distinguish between functional components and class components

// Distinguish whether it is a component
export function isComponent(virtualDOM){
    return virtualDOM&& typeof virtualDOM.type === 'function'
}
// Check whether it is a function component
export function isFunctionComponent(virtualDOM){
    / / (virtualDOM prototype && virtualDOM. The prototype. The render) is a kind of component,
    // Since the class Component has the render method, it inherits from the parent class (Component) even if it is not defined
    returnisComponent(virtualDOM)&& ! (virtualDOM.type.prototype && virtualDOM.type.prototype.render) }Copy the code

Start by modifying the previous code

  • Check if it is a component and call if it is notmounNativeElement
  • If it is a componentmountComponent
    • Determine if it is a function component
    • Function component callbuildFunctionComponent
    • Class component callbuildClassComponent
    • Finally, the virtual DOM call will be returnedmountElement
      • There’s no direct call heremounNativeElementBecause a component might return a component
function mountElement(virtualDOM, container) {
    if (isComponent(virtualDOM)) {
        mountComponent(virtualDOM, container)
    } else {
        mounNativeElement(virtualDOM, container)
    }
}

function mounNativeElement(virtualDOM, container) {
    const dom = createDOM(virtualDOM)
    container.appendChild(dom)
}

function mountComponent(virtualDOM, container) {
    let newVirtualDOM
    if (isFunctionComponent) {
        newVirtualDOM = buildFunctionComponent(virtualDOM)
    } else {
        newVirtualDOM = buildClassComponent(virtualDOM)
    }
    mountElement(newVirtualDOM, container)
}

Copy the code

Processing function component

Function component processing is very simple, just call the function component, return the function component return virtualDOM

function buildFunctionComponent(virtualDOM) {
    return virtualDOM.type(virtualDOM.props || {})
}
Copy the code
function App(){
    return (<div>123</div>)
}

SpReact.render(<App/>.document.getElementById('container'))
Copy the code

Processing class component

You first need to define the parent class of the class component

export default class Component {
    constructor(props){
        this.props = props
    }
    render(){}}Copy the code

Returns VirtualDOM of the component’s Render function

function buildClassComponent(virtualDOM) {
    const component = new virtualDOM.type(virtualDOM.props)
    const newVirtualDOM = component.render()
    return newVirtualDOM
}
Copy the code
import SpReact from "./SpReact"

export default class App extends SpReact.Component{
    render() {
        return (<h1>{this.props.title}</h1>)}}Copy the code

update

const jsx = (<div className="aa"><h1 id="title">VirtualDOM</h1><div>Create VirtualDOM</div></div>)
const jsx1 = (<div className="aa"><h1 id="title">VirtualDOM123</h1><div>Create VirtualDOM123</div></div>)

SpReact.render(jsx, container)
setTimeout(() = > {
    SpReact.render(jsx1, container)
},1000)
Copy the code

VitrualDOM object comparison

export default function diff(virtualDOM, container, oldDOM) {
    if(! oldDOM) { mountElement(virtualDOM, container) }else {
        // VirtualDOM object alignment}}Copy the code

To compare, we need to get the old and new VirtualDOM objects for comparison. Therefore, we need to save the VirtualDOM object when creating the DOM object.

Here you save the VirtualDOM object on the DOM

function createDOM(virtualDOM) {
    const type = virtualDOM.type
    let el
    if (type === 'text') {
        el = document.createTextNode(virtualDOM.props.textContent)
    } else {
        el = document.createElement(type)
        updateNodeElement(el, virtualDOM)
    }
    virtualDOM.children.forEach(child= > {
        mountElement(child, el)
    })
    el.__virtualDOM = virtualDOM
    return el
}
Copy the code

Same Node type

Next, handle the same node type

  • The current node does not need to be updated; the attribute event of the node needs to be updated
  • If the nodes are the same, you need to compare the child nodes
  • If a node is redundant, you need to delete it
export default function diff(virtualDOM, container, oldDOM) {
    if(! oldDOM) { mountElement(virtualDOM, container) }else {
        const oldVirtualDOM = oldDOM.__virtualDOM

        // If type is the same, the current node does not need to be updated
        if (oldVirtualDOM.type === virtualDOM.type) {
            // Update the text
            if (virtualDOM.type === 'text') {
                updateNodeText(virtualDOM, oldVirtualDOM, oldDOM)
            } else {
                // Update the events and properties on the node
                updateNodeElement(oldDOM, virtualDOM)
            }
            // Compare the nodes
            const oldChildNodes = oldDOM.childNodes
            virtualDOM.children.forEach((child, index) = > {
                diff(child, oldDOM, oldChildNodes[index])
            })
            // If a node becomes redundant, delete it
            if (oldChildNodes.length > virtualDOM.children.length) {
                for (let i = oldChildNodes.length - 1, len = virtualDOM.children.length; i > len - 1; i--) {
                    unmountElement(oldChildNodes[i])
                }
            }
        }
    }
}
Copy the code
// Update the text node
export function updateNodeText(virtualDOM, oldVirtualDOM, oldDOM) {
    if (virtualDOM.props.textContent !== oldVirtualDOM.props.textContent) {
        oldDOM.textContent = virtualDOM.props.textContent
        oldDOM.__virtualDOM = virtualDOM
    }
}
// Delete a node
export function unmountElement(el) {
    el.remove()
}
Copy the code

Update the events on the element to refine the original updateNodeElement method

export function updateNodeElement(el, virtualDOM, oldVirtualDOM) {
    const props = virtualDOM.props || {}
    const oldProps = oldVirtualDOM && oldVirtualDOM.props || {}
    Object.keys(virtualDOM.props).forEach(propName= > {
        const val = props[propName]
        const oldVal = oldProps[propName]
        if (val === oldVal) {
            return
        }
        if (propName.startsWith('on')) {
            const eventName = propName.slice(2).toLowerCase()
            el.addEventListener(eventName, val)
            if (oldVal) {
                el.removeEventListener(eventName, oldVal)
            }
        } else if (propName === 'className') {
            el.setAttribute('class', val)
        } else if(propName ! = ='children') {
            el.setAttribute(propName, val)
        }
    })

    // Remove properties that exist in old VirtualDOM objects but do not exist in new VirtualDOM objects
    Remove unnecessary events and attributes from the DOM
    Object.keys(oldProps).forEach(oldPropName= > {
        const oldVal = oldProps[oldPropName]
        const val = props[oldPropName]
        if(! val) {if (oldPropName.startsWith('on')) {
                const oldEventName = oldPropName.slice(2).toLowerCase()
                el.removeEventListener(oldEventName, oldVal)
            } else if (oldPropName === 'className') {
                el.removeAttribute('class', oldVal)
            } else if(propName ! = ='children') {
                el.removeAttribute(oldPropName, oldVal)
            }
        }
    })
}
Copy the code

The node type is different

Nodes of different types are not components. Create a new VirtualDOM and replace the original DOM element

export default function diff(virtualDOM, container, oldDOM) {
    if(! oldDOM) { mountElement(virtualDOM, container) }else {
        const oldVirtualDOM = oldDOM.__virtualDOM

        if (oldVirtualDOM.type === virtualDOM.type) {
            if (virtualDOM.type === 'text') {
                updateNodeText(virtualDOM, oldVirtualDOM, oldDOM)
            } else {
                updateNodeElement(oldDOM, virtualDOM, oldVirtualDOM)
            }
        } else if(! isComponent(virtualDOM)) {const el = createDOM(virtualDOM)
            container.replaceChild(el, oldDOM)
        }

        const oldChildNodes = oldDOM.childNodes
        virtualDOM.children.forEach((child, index) = > {
            diff(child, oldDOM, oldChildNodes[index])
        })

        if (oldChildNodes.length > virtualDOM.children.length) {
            for (let i = oldChildNodes.length - 1, len = virtualDOM.children.length; i > len - 1; i--) {
                unmountElement(oldChildNodes[i])
            }
        }
    }
}
Copy the code

Component updates

SetState update

You need to declare the setState method on the parent of the child component, change the state value of the component, call the render method of the component to produce a new VirtualDOM object, and then update the page DOM element by comparing the old and new VirtualDOM objects

export default class Component {
    constructor(props) {
        this.props = props
    }
    setState(state) {
        this.state = { ... this.state, ... state }// Generate a new VirtualDOM object
        const virtualDOM = this.render()
        // Get the old DOM object
        const oldDOM = this.getDOM()
        // Add the Component property to virtualDOM
        virtualDOM.component = this
        / / than
        diff(virtualDOM, oldDOM.parentNode, oldDOM)
    }
    setDOM(dom) {
        this.__DOM = dom
    }
    getDOM() {
        return this.__DOM
    }
    render(){}Copy the code

Save the component to a new VirtualDOM object

function buildClassComponent(virtualDOM) {
    const component = new virtualDOM.type(virtualDOM.props)
    const newVirtualDOM = component.render()
    newVirtualDOM.component = component
    return newVirtualDOM
}
Copy the code

After creating the VirtualDOM object’s DOM element, you need to save the component’s DOM element to the component if it is the corresponding VirtualDOM object.

function mounNativeElement(virtualDOM, container) {
    const dom = createDOM(virtualDOM)
    container.appendChild(dom)
    const component = virtualDOM.component
    if (component) {
        component.setDOM(dom)
    }
}
Copy the code

Check whether components are the same node

SpReact.render(<App title="App"/>, container)
setTimeout(() = > {
    SpReact.render(<App title="App1"/>, container)
},1000)
Copy the code

If the new VirtualDOM object is a component

export default function diff(virtualDOM, container, oldDOM) {
    if(! oldDOM) { mountElement(virtualDOM, container) }else {
        const oldVirtualDOM = oldDOM.__virtualDOM

        if (oldVirtualDOM.type === virtualDOM.type) {
            if (virtualDOM.type === 'text') {
                updateNodeText(virtualDOM, oldVirtualDOM, oldDOM)
            } else {
                updateNodeElement(oldDOM, virtualDOM, oldVirtualDOM)
            }
        } else if(! isComponent(virtualDOM)) {const el = createDOM(virtualDOM)
            container.replaceChild(el, oldDOM)
        } else {
            diffComponent(virtualDOM, oldVirtualDOM, oldDOM, container)
        }

        const oldChildNodes = oldDOM.childNodes
        virtualDOM.children.forEach((child, index) = > {
            diff(child, oldDOM, oldChildNodes[index])
        })

        if (oldChildNodes.length > virtualDOM.children.length) {
            for (let i = oldChildNodes.length - 1, len = virtualDOM.children.length; i > len - 1; i--) {
                unmountElement(oldChildNodes[i])
            }
        }
    }
}
Copy the code

Check whether it is the same component

export function isSameComponent(virtualDOM,oldVirtualDOM){
    const oldComponent = oldVirtualDOM.component
    return oldComponent && virtualDOM.type === oldComponent.constructor
}
Copy the code

If the old and new VirtualDOM are not the same component, create the DOM corresponding to the node of the new VirtualDOM object and delete the oldDOM

function diffComponent(virtualDOM, oldVirtualDOM, oldDOM, container) {
    if (isSameComponent(virtualDOM, oldVirtualDOM)) {
        updateComponent(virtualDOM, oldVirtualDOM, oldDOM, container)
    } else {
        // There is no DOM element created directly,
        // This is because there is a lot of logic in creating a component's CORRESPONDING DOM element, such as saving the Component to a VirtualDOM object
        // The original logic can be improved, reuse the previous logic
        mountElement(virtualDOM, container, oldDOM)
    }
}
Copy the code

After mountElement, mounNativeElement is eventually called, where the oldDOM object is removed from the mounNativeElement

export function mountElement(virtualDOM, container, oldDOM) {
    if (isComponent(virtualDOM)) {
        mountComponent(virtualDOM, container, oldDOM)
    } else {
        mounNativeElement(virtualDOM, container, oldDOM)
    }
}

export function mountComponent(virtualDOM, container, oldDOM) {
    let newVirtualDOM

    if (isFunctionComponent(virtualDOM)) {
        newVirtualDOM = buildFunctionComponent(virtualDOM)
    } else {
        newVirtualDOM = buildClassComponent(virtualDOM)
    }
    mountElement(newVirtualDOM, container, oldDOM)
}

export function mounNativeElement(virtualDOM, container, oldDOM) {
    const dom = createDOM(virtualDOM)
    container.appendChild(dom)
    if (oldDOM) {
        unmountElement(oldDOM)
    }
    const component = virtualDOM.component
    if (component) {
        component.setDOM(dom)
    }
}
Copy the code

If it is the same component, update the props of the component and regenerate the virtualDOM object for comparison

export function updateComponent(virtualDOM, oldVirtualDOM, oldDOM, container) {
    const oldComponent = oldVirtualDOM.component
    // Update the props of the component
    oldComponent.updateProps(virtualDOM.props)
    const nextVirtualDOM = oldComponent.render()
    nextVirtualDOM.component = oldComponent
    diff(nextVirtualDOM, container, oldDOM)
}
Copy the code

To call the lifecycle function, first add the lifecycle function to the Component class

export function updateComponent(virtualDOM, oldVirtualDOM, oldDOM, container) {
    const oldComponent = oldVirtualDOM.component
    oldComponent.componentWillReceiveProps()
    let props = virtualDOM.props
    let oldProps = oldVirtualDOM.props
    if (oldComponent.shouldComponentUpdate(props, oldProps)) {
        oldComponent.componentWillUpdate(props)
        oldComponent.updateProps(virtualDOM.props)
        const nextVirtualDOM = oldComponent.render()
        nextVirtualDOM.component = oldComponent
        diff(nextVirtualDOM, container, oldDOM)
        oldComponent.componentDidUpdate(oldProps)
    }
}
Copy the code

The ref attribute

<h1 ref={title= > this.title = title}>{this.props.title}</h1>
Copy the code

When a DOM element is created, the function corresponding to the ref attribute is executed

export function createDOM(virtualDOM) {
    const type = virtualDOM.type
    let el
    if (type === 'text') {
        el = document.createTextNode(virtualDOM.props.textContent)
    } else {
        el = document.createElement(type)
        updateNodeElement(el, virtualDOM)
    }
    virtualDOM.children.forEach(child= > {
        mountElement(child, el)
    })
    if (virtualDOM && virtualDOM.props && virtualDOM.props.ref) {
        virtualDOM.props.ref(el)
    }
    el.__virtualDOM = virtualDOM
    return el
}
Copy the code

Add a ref attribute to the component, and the value of the ref attribute is the component

<Alert ref={alert= > this.alert = alert}/>
Copy the code
export function mountComponent(virtualDOM, container, oldDOM) {
    let newVirtualDOM

    if (isFunctionComponent(virtualDOM)) {
        newVirtualDOM = buildFunctionComponent(virtualDOM)
    } else {
        newVirtualDOM = buildClassComponent(virtualDOM)
    }
   
    mountElement(newVirtualDOM, container, oldDOM)

    const component = newVirtualDOM.component
    // If it is a component, the component lifecycle function is called
    If the ref attribute is present in the props, the function corresponding to the ref attribute is called
    if(component){
        component.componentDidMount()
        if (component.props && component.props.ref) {
            component.props.ref(component)
        }
    }
}
Copy the code

Key attribute alignment

Children of the same parent node can compare VirtualDOM objects using the key attribute

export default function diff(virtualDOM, container, oldDOM) { if (! oldDOM) { mountElement(virtualDOM, container) } else { const oldVirtualDOM = oldDOM.__virtualDOM if (oldVirtualDOM.type=== virtualDOM.type) {
            if (virtualDOM.type === 'text') {
                updateNodeText(virtualDOM, oldVirtualDOM, oldDOM)
            } else {
                updateNodeElement(oldDOM, virtualDOM, oldVirtualDOM)
            }
- const oldChildNodes = oldDOM.childNodes
- virtualDOM.children.forEach((child, index) => {
- diff(child, oldDOM, oldChildNodes[index])
-})// compare the child nodes+ diffChildren(virtualDOM, oldDOM)

- if (oldChildNodes.length > virtualDOM.children.length) {
- for (let i = oldChildNodes.length - 1, len = virtualDOM.children.length; i > len - 1; i--) {
- unmountElement(oldChildNodes[i])
-}
-}} else if (! isComponent(virtualDOM)) {- const el = createDOM(virtualDOM)
- container.replaceChild(el, oldDOM)// The virtualDOM object may have been created by the component. // mounNativeElement will be called to handle the component's logic+ mountElement(virtualDOM, container, oldDOM)
        } else {
            diffComponent(virtualDOM, oldVirtualDOM, oldDOM, container)
        }
    }
}
Copy the code
  • First, check whether there is a key attribute on the old node. If there is no key attribute, use the previous logic (loop call diff for comparison).
  • There is a key attribute
    • Use the key attribute in the new virtualDOM to find the old node, and if it exists, determine whether the location is correct or not, and move the old node to the correct location
    • Create the corresponding virtualDOM if it does not exist
function diffChildren(virtualDOM, oldDOM) {
    const oldChildNodes = oldDOM.childNodes

    let keyElements = {}
    for (let i = 0, len = oldChildNodes.length; i < len; i++) {
        const el = oldChildNodes[i]
        if (el.nodeType === 1) {
            const key = el.getAttribute('key')
            if (key) {
                keyElements[key] = el
            }
        }
    }

    const hasNoKey = Object.keys(keyElements).length === 0

    if (hasNoKey) {
        virtualDOM.children.forEach((child, index) = > {
            diff(child, oldDOM, oldChildNodes[index])
        })
    } else {
        // There is a key attribute
        virtualDOM.children.forEach((child, index) = > {
            const key = child.props.key
            if (key) {
                const el = keyElements[key]
                if (el) {
                    // Move the oldChilNodes position
                    if(el ! == oldChildNodes[index]) oldDOM.insertBefore(el, oldChildNodes[index])/ / than
                    else diff(child, oldDOM, el)
                } else {
                    // Create it again
                    mountElement(child, oldDOM, oldChildNodes[index], true)}}})}if (oldChildNodes.length > virtualDOM.children.length) {
        for (let i = oldChildNodes.length - 1, len = virtualDOM.children.length; i > len - 1; i--) {
            unmountElement(oldChildNodes[i])
        }
    }
}
Copy the code

MountElement adds a parameter to whether it is new. If it is new, oldDOM will not be deleted when mounNativeElement is called. When inserting an element, you need to insert the new element before oldDOM

export function mountElement(virtualDOM, container, oldDOM, isNew) {
    if (isComponent(virtualDOM)) {
        mountComponent(virtualDOM, container, oldDOM, isNew)
    } else {
        mounNativeElement(virtualDOM, container, oldDOM, isNew)
    }
}

export function mounNativeElement(virtualDOM, container, oldDOM, isNew) {
    const dom = createDOM(virtualDOM)

    if (oldDOM) {
        // Insert the element before the oldDOM element
        container.insertBefore(dom, oldDOM)
        if(! isNew) { unmountElement(oldDOM) } }else {
        container.appendChild(dom)
    }
    const component = virtualDOM.component
    if (component) {
        component.setDOM(dom)
    }
}

export function mountComponent(virtualDOM, container, oldDOM, isNew) {
    let newVirtualDOM

    if (isFunctionComponent(virtualDOM)) {
        newVirtualDOM = buildFunctionComponent(virtualDOM)
    } else {
        newVirtualDOM = buildClassComponent(virtualDOM)
    }

    mountElement(newVirtualDOM, container, oldDOM, isNew)
    const component = newVirtualDOM.component
    if (component) {
        component.componentDidMount()
        if (component.props && component.props.ref) {
            component.props.ref(component)
        }
    }
}
Copy the code

The key property deletes a node

If the key attribute is not present, the previous index is used to remove unnecessary elements

If the key attribute exists, you need to traverse the old node and use the key of the old node to search for the element corresponding to the key attribute in the new node. If the new node does not exist, the current node has been deleted

function diffChildren(virtualDOM, oldDOM) {
    const oldChildNodes = oldDOM.childNodes

    let keyElements = {}
    for (let i = 0, len = oldChildNodes.length; i < len; i++) {
        const el = oldChildNodes[i]
        if (el.nodeType === 1) {
            const key = el.getAttribute('key')
            if (key) {
                keyElements[key] = el
            }
        }
    }

    const hasNoKey = Object.keys(keyElements).length === 0

    if (hasNoKey) {
        // Compare nodes
        virtualDOM.children.forEach((child, index) = > {
            diff(child, oldDOM, oldChildNodes[index])
        })
        // Delete a node
        if (oldChildNodes.length > virtualDOM.children.length) {
            for (let i = oldChildNodes.length - 1, len = virtualDOM.children.length; i > len - 1; i--) {
                unmountElement(oldChildNodes[i])
            }
        }
    } else {
        const newKeyElements = {}
        virtualDOM.children.forEach((child, index) = > {
            const key = child.props.key
            if (key) {
                newKeyElements[key] = child
                const el = keyElements[key]
                if (el) {
                    if(el ! == oldChildNodes[index]) oldDOM.insertBefore(el, oldChildNodes[index])else diff(child, oldDOM, el)
                } else {
                    mountElement(child, oldDOM, oldChildNodes[index], true)}}})for (let i = 0; i < oldChildNodes.length; i++) {
            let oldChild = oldChildNodes[i]
            let oldKey = oldChild.__virtualDOM.props.key
            if(! newKeyElements[oldKey]) { unmountElement(oldChild) } } } }Copy the code

Remove nodes

Deleting a node also requires consideration of whether the node is a node or text

  • Text, delete the node directly
  • Component, which calls the life cycle function that unloads the component
  • If the deleted node has the ref attribute, the ref attribute needs to be deleted
  • If there are events, delete the events of the node
export function unmountElement(el) {
    const virtualDOM = el.__virtualDOM
    if (virtualDOM.type === 'text') {
        el.remove()
        return
    }
    // Not text
    const component = virtualDOM.component
    // Component lifecycle
    if (virtualDOM.component) {
        component.componentWillMount()
    }
    const props = virtualDOM.props
    // Clear the ref attribute
    if (props && props.ref) {
        props.ref(null)}// Delete the event
    Object.keys(props).forEach(propName= > {
        if (propName.startsWith('on')) {
            const event = propName.slice(2).toLowerCase()
            const val = virtualDOM.props[propName]
            el.removeEventListener(event, val)
        }
    })
    // Delete the child node
    if (el.childNodes.length) {
        el.childNodes.forEach(child= > {
            unmountElement(child)
        })
    }
    // Delete the current element
    el.remove()
}
Copy the code