前言
根据前面介绍的图解diff算法,现在对里面的内容进行代码实现
前置
vnode
我对它的见解是:它是一个对象,是对dom元素的一种描述,为了方便对dom的操作所规定的一种数据规范。后续的一切逻辑都是基于这种数据规范进行的
很随意的描述一个dom,不就这样吗
{
"sel": "li",//标签名称
"element": {},//对应的真实dom
"children": [],//子元素集合
"attr": {//绑定的属性
"key": "A"
},
"text": "A"//标签内的文本
}
将真实dom转成vnode对象
//传递dom上的属性就返回一个对其真实dom的描述对象
const createVnode = (sel, element, children, text, attr) => {
return {
sel, element, children, attr, text
}
}
有这么简单?加上考虑到子元素的情况
const getElementAttr = (node) => {
// 获取所有属性
const attributes = node.attributes
const res = {}
// 遍历所有属性
for (let i = 0; i < attributes.length; i++) {
let attr = attributes[i]
res[attr.name] = attr.value
}
return res
}
const realNodeToVnode = (node) => {
const tag = node.tagName.toLowerCase()
const children = node.children
const txt = node.innerText
const vnode = createVnode(tag, node, children, txt, getElementAttr(node))
vnode.children = Array.from(children).map(item => realNodeToVnode(item))
return vnode
}
实际操作一下;
页面上渲染的内容如下
<ul id="box">
<li key="A">A</li>
<li key="B">B</li>
<li key="C">C</li>
<li key="D">
<span key="D-1">D-1</span>
<span key="D-2">D-2</span>
<span key="D-3">D-3</span>
<span key="D-4">D-4</span>
</li>
</ul>
将上面的dom转为vnode
const box = document.querySelector("#box")
const res = realNodeToVnode(box)
debugger
结果如下,将真实dom转换成了 简易的一个vnode
diff算法就是对比两个vnode之间的差异,核心就是尽可能的复用旧的
下面是代码实践。可以打开图解diff算法,对照着下面代码看
1.拿到新旧vnode
这里简单写个h函数
const h = (sel, attr, content) => {
if (typeof content === "string") return createVnode(sel, undefined, [], content, attr)
else if (Array.isArray(content)) {
return createVnode(sel, undefined, content, undefined, attr)
}
}
页面已经渲染的dom元素
<ul id="box">
<li key="A">A</li>
<li key="B">B</li>
<li key="C">C</li>
<li key="D">D</li>
</ul>
生成一个vnode作为将要更新到页面的dom对象
const vnode4 = h('ul', {}, [
h('li', { key: "B" }, 'B'),
h('li', { key: "D" }, 'D'),
h('li', { key: "C" }, "C"),
h('li', { key: "A" }, 'A'),
h('li', { key: "E" }, 'E'),
])
这里声明一个patch函数来接收一下这两个vnode
- 如果两个元素的标签都不一样就完全以新的为准,旧的就不用了
patchNode函数来对两个vnode进一步深入的对比- 我这里其实写的很简易,细节就不写了,只看核心
const patch = (oldNode, newNode) => {
if (!oldNode.sel) {
// 传进来的非虚拟dom
oldNode = realNodeToVnode(oldNode)
}
if (oldNode.sel !== newNode.sel) {//标签都不一样
const newRes = createElment(newNode)
let p = oldNode.element.parentNode
p.insertBefore(newRes.element, oldNode.element)
p.removeChild(oldNode.element)
}
else {
patchNode(oldNode, newNode, oldNode.element)
}
}
const box = document.querySelector("#box")
patch(box, vnode4)
patchNode分步骤看,操作步骤中我先省去递归调用,理清楚只有一个层级的流程再说
- 先简单写个函数
sameNode
const sameNode = (node, nodeTar) => {
let flag = true
if (node.attr.key && nodeTar.attr.key) {
flag = node.attr.key === nodeTar.attr.key
}
return node.text === nodeTar.text && node.sel === nodeTar.sel && flag
}
- 再简单写个
createElment函数,这个用于将vnode创建为真实dom,我觉得这里不应该递归创建所有元素 - 因为diff是同级对比,你把它还没有对比的子元素都创建好了,剩下的怎么玩?
const createElment = (vnode) => {
const parentNode = document.createElement(vnode.sel)
vnode.text && (parentNode.innerText = vnode.text)
if (vnode.children && Array.isArray(vnode.children) && vnode.children.length > 0) {
vnode.children.forEach(item => {
parentNode.appendChild(createElment(item).element)
})
}
vnode.element = parentNode
return vnode
}
旧头->新头
if (sameNode(oldNodeLIst[oldStart], newNodeList[newStart])) {
newNodeList[newStart]['element'] = oldNodeLIst[oldStart]['element']
oldNodeLIst[oldStart]['element'] = null
++newStart
++oldStart
}
旧尾->新尾
else if (sameNode(oldNodeLIst[oldEnd], newNodeList[newEnd])) {
newNodeList[newEnd]['element'] = oldNodeLIst[oldEnd]['element']
oldNodeLIst[oldEnd]['element'] = null
--newEnd
--oldEnd
}
旧头->新尾
else if (sameNode(oldNodeLIst[oldStart], newNodeList[newEnd])) {
newNodeList[newEnd]['element'] = oldNodeLIst[oldStart]['element']
const oldStartEl = oldNodeLIst[oldStart]['element']
const oldendEl = oldNodeLIst[oldEnd]['element'].nextSibling
parentNode.insertBefore(oldStartEl, oldStartEl)
oldNodeLIst[oldStart]['element'] = null
++oldStart
--newEnd
}
旧尾->新头
else if (sameNode(oldNodeLIst[oldEnd], newNodeList[newStart])) {
newNodeList[newStart]['element'] = oldNodeLIst[oldEnd]['element']
parentNode.insertBefore(oldNodeLIst[oldEnd]['element'], oldNodeLIst[oldStart]['element'])
oldNodeLIst[oldEnd]['element'] = null
++newStart
--oldEnd
}
以上都不满足?
- 查找是否存在相同节点
const res = oldNodeLIst.findIndex(item => sameNode(newNodeList[newStart], item))
if (res !== -1) {
newNodeList[newStart]['element'] = oldNodeLIst[res]['element']
if (res !== 0) {
parentNode.insertBefore(oldNodeLIst[res]['element'], oldNodeLIst[oldStart]['element'])
}
oldNodeLIst[res]['element'] = null
++newStart
}
还是找不到可以复用的?那就自己创建
const newRes = createElment(newNodeList[newStart])
parentNode.insertBefore(newRes.element, oldNodeLIst[oldStart]['element'])
++newStart
完整代码
const createVnode = (sel, element, children, text, attr) => {
return {
sel, element, children, attr, text
}
}
const h = (sel, attr, content) => {
if (typeof content === "string") return createVnode(sel, undefined, [], content, attr)
else if (Array.isArray(content)) {
return createVnode(sel, undefined, content, undefined, attr)
}
}
const createElment = (vnode) => {
const parentNode = document.createElement(vnode.sel)
vnode.text && (parentNode.innerText = vnode.text)
if (vnode.children && Array.isArray(vnode.children) && vnode.children.length > 0) {
vnode.children.forEach(item => {
parentNode.appendChild(createElment(item).element)
})
}
vnode.element = parentNode
return vnode
}
const sameNode = (node, nodeTar) => {
let flag = true
if (node.attr.key && nodeTar.attr.key) {
flag = node.attr.key === nodeTar.attr.key
}
return node.text === nodeTar.text && node.sel === nodeTar.sel && flag
}
const patchNode = (oldNode, newNode, parentNode) => {
if (newNode.children && newNode.children.length > 0 && oldNode.children && oldNode.children.length > 0) {
let newNodeList = newNode.children
let oldNodeLIst = oldNode.children
let oldStart = 0
let newStart = 0
let oldEnd = oldNode.children.length - 1
let newEnd = newNode.children.length - 1
while (newEnd >= newStart) {
// 旧头->新头
if (sameNode(oldNodeLIst[oldStart], newNodeList[newStart])) {
newNodeList[newStart]['element'] = oldNodeLIst[oldStart]['element']
patchNode(oldNodeLIst[oldStart], newNodeList[newStart], newNodeList[newStart]['element'])
oldNodeLIst[oldStart]['element'] = null
++newStart
++oldStart
}
// 旧尾->新尾
else if (sameNode(oldNodeLIst[oldEnd], newNodeList[newEnd])) {
newNodeList[newEnd]['element'] = oldNodeLIst[oldEnd]['element']
patchNode(oldNodeLIst[oldEnd], newNodeList[newEnd], newNodeList[newEnd]['element'])
oldNodeLIst[oldEnd]['element'] = null
--newEnd
--oldEnd
}
// 旧头->新尾
else if (sameNode(oldNodeLIst[oldStart], newNodeList[newEnd])) {
newNodeList[newEnd]['element'] = oldNodeLIst[oldStart]['element']
parentNode.insertBefore(oldNodeLIst[oldStart]['element'], oldNodeLIst[oldEnd]['element'].nextSibling)
patchNode(oldNodeLIst[oldStart], newNodeList[newEnd], newNodeList[newEnd]['element'])
oldNodeLIst[oldStart]['element'] = null
++oldStart
--newEnd
}
// 旧尾->新头
else if (sameNode(oldNodeLIst[oldEnd], newNodeList[newStart])) {
newNodeList[newStart]['element'] = oldNodeLIst[oldEnd]['element']
parentNode.insertBefore(oldNodeLIst[oldEnd]['element'], oldNodeLIst[oldStart]['element'])
patchNode(oldNodeLIst[oldEnd], newNodeList[newStart], newNodeList[newStart]['element'])
oldNodeLIst[oldEnd]['element'] = null
++newStart
--oldEnd
}
// 查找是否存在相同节点
else {
const res = oldNodeLIst.findIndex(item => sameNode(newNodeList[newStart], item))
if (res !== -1) {
newNodeList[newStart]['element'] = oldNodeLIst[res]['element']
if (res !== 0) {
parentNode.insertBefore(oldNodeLIst[res]['element'], oldNodeLIst[oldStart]['element'])
}
patchNode(oldNodeLIst[res]['element'], newNodeList[newStart]['element'], newNodeList[newStart]['element'])
oldNodeLIst[res]['element'] = null
++newStart
} else {
const newRes = createElment(newNodeList[newStart])
parentNode.insertBefore(newRes.element, oldNodeLIst[oldStart]['element'])
++newStart
}
}
}
// 结束之后,清除旧节点中没有复用的节点
while (oldStart <= oldEnd) {
oldNodeLIst[oldStart]['element'] && parentNode.removeChild(oldNodeLIst[oldStart]['element'])
++oldStart
}
} else if (newNode.children && newNode.children.length > 0 && (!oldNode.children || oldNode.children.length === 0)) {
newNode.children.forEach(item => {
const newRes = createElment(item)
parentNode.insertBefore(newRes.element, parentNode.children.length > 0 ? parentNode.children[parentNode.children.length - 1] : null)
})
}
else {
for (let i = 0; i < parentNode.children.length; i++) {
parentNode.removeChild(parentNode.children[0])
}
}
}
const getElementAttr = (node) => {
// 获取所有属性
const attributes = node.attributes
const res = {}
// 遍历所有属性
for (let i = 0; i < attributes.length; i++) {
let attr = attributes[i]
res[attr.name] = attr.value
}
return res
}
const realNodeToVnode = (node) => {
const tag = node.tagName.toLowerCase()
const children = node.children
const txt = node.innerText
const vnode = createVnode(tag, node, children, txt, getElementAttr(node))
vnode.children = Array.from(children).map(item => realNodeToVnode(item))
return vnode
}
// oldNode一定在页面存在真实节点
const patch = (oldNode, newNode) => {
if (!oldNode.sel) {
// 传进来的非虚拟dom
oldNode = realNodeToVnode(oldNode)
}
if (oldNode.sel !== newNode.sel) {
const newRes = createElment(newNode)
let p = oldNode.element.parentNode
p.insertBefore(newRes.element, oldNode.element)
p.removeChild(oldNode.element)
}
else {
patchNode(oldNode, newNode, oldNode.element)
}
}
const box = document.querySelector("#box")
const vnode4 = h('ul', {}, [
h('li', { key: "B" }, 'B'),
h('li', { key: "D" }, 'D'),
h('li', { key: "C" }, "C"),
h('li', { key: "A" }, 'A'),
h('li', { key: "E" }, 'E'),
])
patch(box, vnode4)
我只是关注核心对比部分,没有过多关注其他细节地方,多多指正!!!