乐趣区

关于javascript:snabbdom-简单实现

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>
    <script src="./js/index.js"></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) => {
    // 实在 dom 转换为虚构 dom
    if (oldVNode.sel == undefined) {let sel = oldVNode.tagName.toLowerCase()
        let data = {}
        let children = []
        let text = undefined
        oldVNode = VNode(sel, data, children, text, oldVNode)
    }

    // 新旧 tagName 统一
    if (oldVNode.sel === newVNode.sel) {patchVNode(oldVNode, newVNode)
    } else {
        // 新旧 tagName 不统一 间接删除旧的创立新的
        // 依据虚构 dom 创立 dom 节点
        let newVNodeElm = createElement(newVNode)
        let oldVNodeElm = oldVNode.elm
        // 插入新的 dom
        if (newVNodeElm) {oldVNodeElm.parentNode.insertBefore(newVNodeElm, oldVNodeElm)
        }
        // 删除旧 dom
        oldVNodeElm.parentNode.removeChild(oldVNodeElm)
    }
}

function patchVNode(oldVNode, newVNode) {
    // 新的没有 child 间接替换旧的内容
    if (newVNode.children === undefined) {if (newVNode.text !== oldVNode.text) {oldVNode.elm.innerText = newVNode.text}
    } else {
        // 新旧都有子元素
        if (oldVNode.children !== undefined && oldVNode.children.length > 0) {
            let parentDom = oldVNode.elm
            let oldCh = oldVNode.children
            let newCh = newVNode.children

            let oldStartIdx = 0 // 旧前
            let oldEndIdx = oldCh.length - 1 // 旧后
            let newStartIdx = 0 // 新前
            let newEndIdx = newCh.length - 1 // 新后

            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 {
            // 新的有子元素,循环插入

            // 清空旧节点
            oldVNode.elm.innerHTML = ''
            // 便当插入新的子元素
            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}
}
退出移动版