1、创建 Virtual Dom 及辅助函数
// 创建vdom,正常html标签是对象,文本节点是字符串
function createVdom(tag, attrs, children) {
return {
tag,
attrs,
children
}
}
// vdom 转为真实 dom
function vdom2Element(vdom) {
if (typeof vdom === 'string') {
return document.createTextNode(vdom)
}
var dom = document.createElement(vdom.tag);
setAttributes(dom, vdom.attrs)
vdom.children.forEach(child => dom.appendChild(vdom2Element(child)))
return dom
}
function setAttributes(dom, attrs) {
Object.keys(attrs).forEach(key => {
var value = attrs[key]
// 事件
if (/^on[A-Z]+/.test(key)) {
dom.addEventListener(key.slice(2).toLowerCase(), value, false)
} else if (key === 'class') {
// 类名
dom.classList.add(value)
} else if (key === 'style') {
// 样式
dom.style.cssText = value
} else {
// 通用
dom.setAttribute(key, value)
}
})
}
// 挂载节点
function mount(dom, container) {
container.appendChild(dom)
}
2、实现 diff 算法
/**
* 总体采用先序深度优先遍历算法得出2棵树的差异
*/
// 对比新旧vdom
function diff(old, fresh) {
var patches = {}
var index = 0
walk(old, fresh, index, patches)
return patches
}
function walk(old, fresh, index, patches) {
var muteArray = []
// 新节点不存在
if (!fresh) {
muteArray.push({
type: 'REMOVE'
})
} else if (typeof fresh === 'string') {
// 新节点是文本节点
muteArray.push({
type: "REPLACE",
value: fresh
})
} else {
// 新节点是 tag 节点
if (typeof old === 'string') {
// 老节点是文本节点
muteArray.push({
type: "REPLACE",
value: fresh
})
} else {
// 老节点是tag节点
// 比较属性
var attrs = diffAttr(old.attrs, fresh.attrs)
muteArray.push({
type: 'ATTR',
value: attrs
})
// 比较子节点
old.children.forEach((child, key) => {
walk(child, fresh.children[key], index++, patches)
})
}
}
// 记录当前节点的变化
if (muteArray.length >= 1) {
patches[index] = muteArray
}
}
function diffAttr(oldAttr, freshAttr) {
var result = Object.create(null)
// 覆盖老的
for (const key in oldAttr) {
if (oldAttr.hasOwnProperty(key)) {
result[key] = freshAttr[key]
}
}
// 获取新的
for (const key in freshAttr) {
if (!oldAttr.hasOwnProperty(key)) {
result[key] = freshAttr[key]
}
}
return result
}
3、实现 patch 算法
function patch(el, patches) {
var index = 0;
traverse(el, index, patches)
}
function traverse(el, index, patches) {
var children = el.childNodes
// 先序深度优先
children.forEach(child => {
doPatch(child, index++, patches)
})
doPatch(el, index, patches)
}
function doPatch(el, index, patches) {
var patch = patches[index]
patch.forEach(item => {
if (item.type === 'REMOVE') {
el.parentNode.removeChild(el)
} else if (item.type === 'REPLACE') {
const newDom = vdom2Element(item.value)
el.parentNode.replaceChild(newDom, el)
} else {
setAttributes(el, item.value)
}
})
}
4、测试
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>diff-patch demo</title>
<style>
.red {
color: red
}
.black {
color: black;
}
</style>
<script src="./element.js"></script>
<script src="./diff.js"></script>
<script src="./patch.js"></script>
</head>
<body>
<div id="app"></div>
<div id="diff"></div>
<button onclick="Mount()">挂载</button>
<button onclick="Diff()">diff</button>
<button onclick="Patch()">patch</button>
<pre>
<code>
// 1、初次渲染
var vdom1 = createVdom('h1', {
class: 'red'
}, [createVdom('p', {
class: 'red'
}, ['test']), createVdom('p', {
class: 'red',
style: 'color:green'
}, ['test2']), createVdom('p', {
class: 'red',
style: 'color:green',
onClick: function () {
console.log('clicked')
},
name: 'test'
}, ['test3'])])
const el = vdom2Element(vdom1)
mount(el, document.querySelector('#app'))
</code>
</pre>
<pre>
<code>
// 2、diff-patch
var vdom2 = createVdom('h1', {
class: 'black'
}, [createVdom('p', {}, ['testtest']), createVdom('p', {}, ['test2test2']), createVdom('p', {}, ['test2'])])
const patches = diff(vdom1, vdom2)
console.log('patches', patches)
patch(el, patches)
</code>
</pre>
<script>
// 1、初次渲染
var vdom1 = createVdom('h1', {
class: 'red'
}, [createVdom('p', {
class: 'red'
}, ['test']), createVdom('p', {
class: 'red',
style: 'color:green'
}, ['test2']), createVdom('p', {
class: 'red',
style: 'color:green',
onClick: function () {
console.log('clicked')
},
name: 'test'
}, ['test3'])])
// const el = vdom2Element(vdom1)
// mount(el, document.querySelector('#app'))
// 2、diff-patch
var vdom2 = createVdom('h1', {
class: 'black'
}, [createVdom('p', {}, ['testtest']), createVdom('p', {}, ['test2test2']), createVdom('p', {}, ['test2'])])
// const patches = diff(vdom1, vdom2)
// console.log('patches', patches)
// patch(el, patches)
</script>
<script>
var el, patches
function Mount() {
el = vdom2Element(vdom1)
mount(el, document.querySelector('#app'))
}
function Diff() {
patches = diff(vdom1, vdom2)
document.querySelector('#diff').innerHTML = JSON.stringify(patches, '', 4)
}
function Patch() {
patch(el, patches)
}
</script>
</body>
</html>
参考
网友评论