Handwritten diff algorithm

Posted by wiggly81 on Wed, 15 Sep 2021 19:08:21 +0200

Environment Setup

  • Create a folder, go to the folder directory, and use NPM init-y to generate the package.json file.
    Download webpack, webpack-cli, webpack-dev-server, with particular attention to the various versions of webpack.
    Download the corresponding version
cnpm i webpack@5 webpack-cli@3 webpack-dev-server@3 -S
  • Create webpack.config.js file
module.exports = {
    entry: {
        index: './src/index.js'
    },
    output: {
        path: __dirname + '/public',
        filename: './js/[name].js'
    },
    devServer: {
        contentBase: './public',
        inline: true
    }
}
  • Create a file based on the contents of the webpack.config.js file
    File directory after creation
  • Introducing a js file into the index.html file
  • Run the program
    Run commands need to be added to the package.json file
{
  "name": "diff",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "webpack": "^5.52.1",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.2"
  }
}
  • Run the program
    npm run dev

Create h Function

  • Create a new folder dom under src, create a new file h.js
import vnode from './vnode'
export default function(sel, data, params) {
    // console.log(sel, data, params)
    // Without child elements
    if(typeof params === 'string') {
        return vnode(sel, data, undefined, params, undefined)
    } else if(Array.isArray(params)) { // With child elements
        let children = []
        for(let item of params) {
            children.push(item)
        }
        return vnode(sel, data, children, undefined, undefined)
    }
}
  • Create a new vnode.js under the dom folder, which converts a real dom into a virtual dom
export default function(sel, data, children, text, elm) {
    /**
     * sel: Virtual dom
     * data: key
     * children:  Child Elements
     * text: Text Content
     * elm: True dom
     */

    return {
        sel,
       data,
       children,
       text,
       elm
    }
}
  • Create virtual dom in index.js
import h from './dom/h'

let vnode1 = h("div", {}, "How do you do");
console.log(vnode1)

let vnode2 = h("ul", {}, [
    h("li", {}, "a"),
    h("li", {}, "b"),
    h("li", {}, "c"),
    h("li", {}, "d"),
    h("li", {}, "e"),
]);

console.log(vnode2)
  • Run Results

Turn a real dom into a virtual dom and compare the old and new virtual nodes

  • If the old and new nodes are not the same node name, then the old node is deleted and the new node is inserted violently for a long time.
    1. Write a new div under index.html
 <div id="container">This is container</div>

2. Get the div real dom node under index.html under index.js

// Get the real dom node
const container = document.getElementById('container')

3. Create a virtual node to patch the virtual node onto the real dom

// Virtual Node
let vnode1 = h("h1", {}, "How do you do");
// console.log(vnode1)

let vnode2 = h("ul", {}, [
    h("li", {}, "a"),
    h("li", {}, "b"),
    h("li", {}, "c"),
    h("li", {}, "d"),
    h("li", {}, "e"),
]);
patch(container, vnode1)

4. Create a new patch.js function to convert a real DOM into a virtual dom, and then compare the new virtual DOM with the old one.

import vnode from './vnode'
import createElement from './createElement'
/**
 * @param {*} oldVnode Old Virtual Node
 * @param {*} newVnode New Virtual Node
 */
export default function (oldVnode, newVnode) {
    // If oldVnode has no sel, it is a non-virtual node
    if (oldVnode.sel === undefined) {
        // console.log(oldVnode.tagName.toLowerCase())
        oldVnode = vnode(
            oldVnode.tagName.toLowerCase(), //Virtual Node Name
            {},
            [],
            undefined,
            oldVnode // Real dom elements
        )
    }
    // console.log(oldVnode)
    // Start judging old and new virtual nodes
    if (oldVnode.sel !== newVnode.sel) { // If the old and new nodes are not the same node name, then the old node is deleted and the new node is inserted violently for a long time.
        // Create a new virtual node as a real dom node
        let newVnodeElm = createElement(newVnode) // It's a good idea to write a function that creates a real dom for recursive calls
        // console.log(newVnodeElm)
        let oldVnodeElm = oldVnode.elm
        if(newVnodeElm) {
            oldVnodeElm.parentNode.insertBefore(newVnodeElm, oldVnodeElm)
        }
        oldVnodeElm.parentNode.removeChild(oldVnodeElm)
    } else { // Is the same node, logic becomes complex

    }
}

5. Create a new createElement.js file to convert virtual nodes into real nodes and mount them on the real dom

export default function createElement (vnode) {
    let domNode = document.createElement(vnode.sel)
    // Determine if there are children
    if(vnode.children === undefined) {
        domNode.innerText = vnode.text
    }else if(Array.isArray(vnode.children)) {
        for(let child of vnode.children) {
            let childDom = createElement(child)
            domNode.appendChild(childDom)
        }
    }
    vnode.elm = domNode
    return domNode
}
  • If the nodes are the same, there are several cases
    • (1) The new node has no children and is replaced directly
    • (2) The new node has children, the old node has children, and the diff algorithm core
    • (3) New node has children, old node does not, directly create element add

Create a new patchVnode.js file to determine if the old and new virtual nodes are the same

import createElement from './createElement'
export default function(oldVnode, newVnode) {
    // Determine if a new node has child elements
    if(newVnode.children === undefined) { // No child elements
        if(newVnode.text !== oldVnode.text) {
            oldVnode.elm.innerText = newVnode.text
        }
    } else { // Has child elements
        // Determine if the old node has child elements
        if(oldVnode.children && oldVnode.children.length > 0) { // Old node has child elements, which is more complex

        } else { // Old node has no child elements
            oldVnode.elm.innerHTML = '' // Delete the contents of old nodes
            for(let child of newVnode.children) {
                let childDom = createElement(child)
                oldVnode.elm.appendChild(childDom)
            }
            console.log(oldVnode.elm)
        }
    }
}

patch.js file

import vnode from './vnode'
import createElement from './createElement'
import patchVnode from './patchVnode'
/**
 * @param {*} oldVnode Old Virtual Node
 * @param {*} newVnode New Virtual Node
 */
export default function (oldVnode, newVnode) {
    // If oldVnode has no sel, it is a non-virtual node
    if (oldVnode.sel === undefined) {
        // console.log(oldVnode.tagName.toLowerCase())
        oldVnode = vnode(
            oldVnode.tagName.toLowerCase(), //Virtual Node Name
            {},
            [],
            undefined,
            oldVnode // Real dom elements
        )
    }
    // console.log(oldVnode)
    // Start judging old and new virtual nodes
    if (oldVnode.sel !== newVnode.sel) { // If the old and new nodes are not the same node name, then the old node is deleted and the new node is inserted violently for a long time.
        // Create a new virtual node as a real dom node
        let newVnodeElm = createElement(newVnode) // It's a good idea to write a function that creates a real dom for recursive calls
        // console.log(newVnodeElm)
        let oldVnodeElm = oldVnode.elm
        if(newVnodeElm) {
            oldVnodeElm.parentNode.insertBefore(newVnodeElm, oldVnodeElm)
        }
        oldVnodeElm.parentNode.removeChild(oldVnodeElm)
    } else { // Is the same node, logic becomes complex
        patchVnode(oldVnode, newVnode)
    }
}

index.js file

import h from './dom/h'
import patch from './dom/patch'

// Virtual Node
// let vnode1 = h("div", {}, "hello");
let vnode1 = h("div", {}, [
    h("span", {}, "a"),
    h("span", {}, "b"),
    h("span", {}, "c"),
    h("span", {}, "d"),
    h("span", {}, "e"),
]);

// Get the real dom node
const container = document.getElementById('container')

patch(container, vnode1)

diff algorithm core

  • 1. Old and New
    Match: Old Front Pointer++, New Front Pointer++.
  • 2. Old and New
    Match: Old Post Pointer -, New Post Pointer -.
  • 3. Before and after the old
    Match: Old Front Pointer++, New Back Pointer-
  • 4. After and Before the Old
    Match: Old Back Pointer - New Front Pointer+.
  • 5. None of the above meets the criteria=== to find
    Find: New pointer++, renders the element pointed to by the new pointer to the page, and assigns undefined to the corresponding element found in the old node
  • 6. Create or delete
    Create a new updateChildren.js to handle five cases in the diff algorithm
import patchVnode from './patchVnode'
import createElement from './createElement'
// Determine whether two virtual nodes are the same node
function sameVnode(vnode1, vnode2) {
    return vnode1.key === vnode2.key
}

/**
 * 
 * @param {*} parentElm Old parent node
 * @param {*} oldCh Old Child Node
 * @param {*} newCh New Child Node
 */
export default function(parentElm, oldCh, newCh) {
    // console.log(parentElm, oldCh, newCh)
    let oldStartIdx = 0 // Old Front Pointer
    let oldEndIdx = oldCh.length - 1 // Old back pointer
    let newStartIdx = 0 // New Front Pointer
    let newEndIdx = newCh.length - 1 // New Post Pointer

    let oldStartVnode = oldCh[0] // Old Former Virtual Node
    let oldEndVnode = oldCh[oldEndIdx] // Old Post Virtual Node
    let newStartVnode = newCh[0] // New Front Virtual Node
    let newEndVnode = newCh[newEndIdx] // New Post Virtual Node

    while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if(oldStartVnode === undefined) {
            oldStartVnode = [++oldStartIdx]
        }
        if(oldEndVnode === undefined) {
            oldEndVnode = [--oldEndIdx]
        }else if(sameVnode(oldStartVnode, newStartVnode)) {
            // Old and New
            console.log(1)
            patchVnode(oldStartVnode, newStartVnode)
            if(newStartVnode) {
                newStartVnode.elm = oldStartVnode?.elm
            }
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        }else if(sameVnode(oldEndVnode, newEndVnode)) {
            // Old and New
            console.log(2)
            patchVnode(oldEndVnode, newEndVnode)
            if(oldEndVnode) {
                newEndVnode.elm = oldEndVnode?.elm
            }
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        }else if(sameVnode(oldStartVnode, newEndVnode)) {
            // Before and after
            console.log(3)
            patchVnode(oldStartVnode, newEndVnode)
            if(oldStartVnode) {
                newEndVnode.elm = oldStartVnode?.elm
            }
            // Move the previously specified node behind the old pointed node
            parentElm.insertBefore( oldStartVnode.elm, oldEndVnode.elm.nextSibling )
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if(sameVnode(oldEndVnode, newStartVnode)) {
            // Old behind and new before
            console.log(4)
            patchVnode(oldEndVnode, newStartVnode)
            if(oldEndVnode) {
                newStartVnode.elm = oldEndVnode?.elm
            }
            // Moves the old specified node to the front of the old pointed node
            parentElm.insertBefore( oldEndVnode.elm, oldStartVnode.elm )
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        } else {
            // None of the above satisfies the criteria=== search
            console.log(5)
            // Create an object storage virtual node
            let keyMap = {}
            for(let i = oldStartIdx;i <= oldEndIdx;i++) {
                const key = oldCh[i]?.key
                if(key) {
                    keyMap[key] = i
                }
            }
            // console.log(keyMap)
            // Finding the Node to which the Front Node Points in the Old Node
            let indxInOld = keyMap[newStartVnode.key]
            if(indxInOld) { // Can find
                const elmMove = oldCh[indxInOld]
                patchVnode(elmMove, newStartVnode)
                // Processed node, set to undefined in the array of old virtual nodes
                oldCh[indxInOld] = undefined
                parentElm.insertBefore(elmMove.elm,  oldStartVnode.elm)
            }else { // Can't find
                // Description is a new node and needs to be recreated
                parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm)
            }
            // New Node Pointer+1
            newStartVnode = newCh[++newStartIdx]
        }
    }
}

Reference updateChildren method in patchVnode.js

import createElement from './createElement'
import updateChildren from './updateChildren'
export default function patchVnode (oldVnode, newVnode) {
    // console.log(oldVnode) 
    // Determine if a new node has child elements
    if (newVnode.children === undefined) { // No child elements
        if (newVnode.text !== oldVnode.text) {
            oldVnode.elm.innerText = newVnode.text
        }
    } else { // Has child elements
        // Determine if the old node has child elements
        if (oldVnode.children && oldVnode.children.length > 0) { // Old node has child elements, which is more complex
            updateChildren(oldVnode.elm, oldVnode.children, newVnode.children)
            // console.log(oldVnode.elm, oldVnode.children, newVnode.children)
        } else { // Old node has no child elements
            oldVnode.elm.innerHTML = '' // Delete the contents of old nodes
            for (let child of newVnode.children) {
                let childDom = createElement(child)
                oldVnode.elm.appendChild(childDom)
            }
            // console.log(oldVnode.elm)
        }
    }
}
  • Create or delete nodes
    Add the following code to updateChild.js
// There are only two cases to end while (add and delete)
    // 1,oldStartIdx > oldEndIdx
    // 2,newStartIdx > newEndIdx
    if(oldStartIdx > oldEndIdx) {
        const before = newCh[newEndIdx + 1] ? newCh[newEndIdx + 1].elm : null
        for(let i = newStartIdx;i <= newEndIdx;i++) {
            parentElm.insertBefore((createElement(newCh[i])), before)
        }
    } else { // Enter Delete Operation
        for(let i = oldStartIdx;i <= oldEndIdx;i++) {
            // console.log(oldCh[i])
            parentElm.removeChild(oldCh[i].elm)
        }
    }

Reference

https://www.bilibili.com/video/BV1K64y1s7ot?p=9&spm_id_from=pageDriver

All Code

gitee address
https://gitee.com/jjm1/diff

Topics: Javascript html