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 }}