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