Simple implementation of snabbdom

Time:2022-5-7

webpack.config.js

module.exports = {
    entry: {
        index: './src/index.js'
    },
    output: {
        path: __dirname + '/public',
        filename: './js/[name].js'
    },
    devServer: {
        contentBase: './public',
        inline: true
    }
}

package.json

{
  "name": "snabbdom",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "webpack-dev-server --open"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "snabbdom": "^3.1.0",
    "webpack": "5",
    "webpack-cli": "3",
    "webpack-dev-server": "3"
  }
}

index.js

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

let container = document.getElementById('container')
let vNode = h('ui', {}, [
    h('li', { key: 'a' }, 'a'),
    h('li', { key: 'b' }, 'b'),
    h('li', { key: 'c' }, 'c'),
    h('li', { key: 'd' }, 'd'),
    h('li', { key: 'e' }, 'e')
])
patch(container, vNode)

let btn = document.getElementById('btn')

console.log(container.nextSibling)

let vNode2 = h('ui', {}, [
    h('li', { key: 'c' }, 'c'),
    h('li', { key: 'b' }, 'b'),
    h('li', { key: 'a' }, 'a'),
    h('li', { key: 'd' }, 'd'),
    h('li', { key: 'e' }, 'e'),
    h('li', { key: 'f' }, 'f'),
])

console.log(vNode2)
btn.onclick = function () {
    patch(vNode, vNode2)
}

public/index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="container">
        <div>1</div>
        <div>2</div>
        <div>3</div>
    </div>

    < button id = "BTN" > button < / button >
    <script></script>
</body>

</html>

dom/h.js

import vNode from './vNode.js'
export default (sel, data, params) => {
    if (typeof params == 'string') {
        let text = params
        return vNode(sel, data, undefined, text, undefined)
    } else if (Array.isArray(params)) {
        let children = []
        for (const item of params) {
            children.push(item)
        }
        return vNode(sel, data, children, undefined, undefined)
    }
}

dom/createElement.js

export default function createElement(vNode) {
    let domNode = document.createElement(vNode.sel)
    if (vNode.children == undefined || vNode.children.length == 0) {
        domNode.innerText = vNode.text
    } else if (Array.isArray(vNode.children)) {
        for (const child of vNode.children) {
            let childDom = createElement(child)
            domNode.appendChild(childDom)
        }
    }
    vNode.elm = domNode
    return domNode
}

dom/patch.js

import VNode from './vNode'
import createElement from './createElement'
export default (oldVNode, newVNode) => {
    //Convert real DOM to virtual DOM
    if (oldVNode.sel == undefined) {
        let sel = oldVNode.tagName.toLowerCase()
        let data = {}
        let children = []
        let text = undefined
        oldVNode = VNode(sel, data, children, text, oldVNode)
    }

    //The old and new tagnames are consistent
    if (oldVNode.sel === newVNode.sel) {
        patchVNode(oldVNode, newVNode)
    } else {
        //The old and new tagnames are inconsistent. Directly delete the old one and create a new one
        //Creating a DOM node from a virtual DOM
        let newVNodeElm = createElement(newVNode)
        let oldVNodeElm = oldVNode.elm
        //Insert a new DOM
        if (newVNodeElm) {
            oldVNodeElm.parentNode.insertBefore(newVNodeElm, oldVNodeElm)
        }
        //Delete old DOM
        oldVNodeElm.parentNode.removeChild(oldVNodeElm)
    }
}

function patchVNode(oldVNode, newVNode) {
    //The new child does not directly replace the old content
    if (newVNode.children === undefined) {
        if (newVNode.text !== oldVNode.text) {
            oldVNode.elm.innerText = newVNode.text
        }
    } else {
        //Both old and new have child elements
        if (oldVNode.children !== undefined && oldVNode.children.length > 0) {
            let parentDom = oldVNode.elm
            let oldCh = oldVNode.children
            let newCh = newVNode.children

            Let oldstartidx = 0 // old
            let oldEndIdx = oldCh. Length - 1 // old rear
            Let newstartidx = 0 // new front
            let newEndIdx = newCh. Length - 1 // after new

            let oldStartVNode = oldCh[0]
            let oldEndVNode = oldCh[oldEndIdx]
            let newStartVNode = newCh[0]
            let newEndVNode = newCh[newEndIdx]

            while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
                if (oldStartVNode == undefined) {
                    oldStartVNode = oldCh[++oldStartIdx]
                } else if (oldEndVNode == undefined) {
                    oldEndVNode = oldCh[--oldEndIdx]
                } else if (sameVNode(oldStartVNode, newStartVNode)) {
                    console.log(1)
                    patchVNode(oldStartVNode, newStartVNode)
                    if (newStartVNode) {
                        newStartVNode.elm = oldStartVNode?.elm
                    }
                    oldStartVNode = oldCh[++oldStartIdx]
                    newStartVNode = newCh[++newStartIdx]
                } else if (sameVNode(oldEndVNode, newEndVNode)) {
                    console.log(2)
                    patchVNode(oldEndVNode, newEndVNode)

                    if (newEndVNode) {
                        newEndVNode.elm = oldEndVNode?.elm
                    }
                    oldEndVNode = oldCh[--oldEndIdx]
                    newEndVNode = newCh[--newEndIdx]
                } else if (sameVNode(oldStartVNode, newEndVNode)) {
                    console.log(3)
                    patchVNode(oldStartVNode, newEndVNode)
                    if (newEndVNode) {
                        newEndVNode.elm = oldStartVNode?.elm
                    }
                    parentDom.insertBefore(
                        oldStartVNode.elm,
                        oldEndVNode.elm.nextSibling
                    )
                    oldStartVNode = oldCh[++oldStartIdx]
                    newEndVNode = newCh[--newEndIdx]
                } else if (sameVNode(oldEndVNode, newStartVNode)) {
                    console.log(4)
                    patchVNode(oldEndVNode, newStartVNode)
                    if (newStartVNode) {
                        newStartVNode.elm = oldEndVNode?.elm
                    }
                    parentDom.insertBefore(
                        oldEndVNode.elm,
                        oldStartVNode.elm.nextSibling
                    )
                    oldEndVNode = oldCh[--oldEndIdx]
                    newStartVNode = newCh[++newStartIdx]
                } else {
                    const keyMap = {}
                    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
                        const key = oldCh[i]?.key
                        if (key) keyMap[key] = i
                    }

                    let idxInOld = keyMap[newStartVNode.key]
                    if (idxInOld) {
                        const elmMove = oldCh[idxInOld]
                        patchVNode(elmMove, newStartVNode)
                        oldCh[idxInOld] = undefined
                        parentDom.insertBefore(elmMove.elm, oldStartVNode.elm)
                    } else {
                        parentDom.insertBefore(
                            createElement(newStartVNode),
                            oldStartVNode.elm
                        )
                    }

                    newStartVNode = newCh[++newStartIdx]
                }
            }

            if (oldStartIdx > oldEndIdx) {
                const before = newCh[newEndIdx + 1]
                    ? newCh[newEndIdx + 1].elm
                    : null

                for (let i = newStartIdx; i <= newEndIdx; i++) {
                    parentDom.insertBefore(createElement(newCh[i]), before)
                }
            } else {
                for (let i = oldStartIdx; i <= oldEndIdx; i++) {
                    parentDom.removeChild(oldCh[i].elm)
                }
            }
        } else {
            //The new has child elements, which are inserted circularly

            //Empty old nodes
            oldVNode.elm.innerHTML = ''
            //Facilitate insertion of new child elements
            for (let iterator of newVNode.children) {
                let childDom = createElement(iterator)
                oldVNode.elm.appendChild(childDom)
            }
        }
    }
}

function sameVNode(vNode1, vNode2) {
    return vNode1.key == vNode2.key
}

dom/vNode.js

export default (sel, data, children, text, elm) => {
    let key = data.key
    return { sel, data, children, text, elm, key }
}