
虚拟DOM和diff算法
wanglin2.github.io/VNode_visua…
虚拟DOM
背景
DOM全称文档对象模型,本质也是一个JS对象。每操作一次DOM都会对页面进行重新渲染,且新生成一颗DOM树。
缺点
- 修改了某个数据,会直接渲染到真实dom上引起整个dom树的重绘和重排
- 原生JS或JQ操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程。在一次操作中,我需要更新10个DOM节点,浏览器收到第一个DOM请求后并不知道还有9次更新操作,因此会马上执行流程,最终执行10次。
前端主流框架 vue 和 react 中都使用了虚拟DOM(virtual DOM)技术,因为渲染真实DOM的开销是很大的,性能代价昂贵,比如有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树的重绘和重排,而我们只需要更新修改过的那一小块dom而不要更新整个dom,这时使用diff算法能够帮助我们。
VDOM
虚拟dom(JS模拟DOM中的真实节点对象), 通过VDom和真实DOM的比对,再通过特定的render方法将其渲染成真实的DOM节点。
- 只会更新对应的节点。diff算法
- 更新10个DOM节点,只会执行最后一次。例如批处理
diff策略
react
传统 diff 算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3),其中 n 是树中节点的总数。O(n^3) 到底有多可怕,这意味着如果要展示1000个节点,就要依次执行上十亿次的比较。
三大策略 将O(n^3)复杂度 转化为 O(n)复杂度
-
策略一(tree diff):新旧DOM树,逐层对比的方式。DOM节点跨层级的操作不做优化,因为很少这么做
1.只会对相同层级的节点进行比较;
2.只有删除、创建操作,没有移动操作;
如图所示,react发现新树中,R节点下没有了A,那么直接删除A,在D节点下创建A以及下属节点。过程就是删除、创建,直接粗暴。
3.由于没做性能优化,所以官方建议少做这样的跨层级操作;
只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。
当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
-
策略二(component diff):
如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
如果不是,那么直接删除旧的,创建新的;
tips: 对于同一个类的组件,用户可以控制其不要进行diff运算,具体就是,用户可以使用
shouldComponentUpdate()来告诉react要不要对此组件进行diff运算。 -
策略三(element diff):
当节点处于同一层级时,拥有同层唯一的key值,来做删除、插入、移动的操作,这是针对element层级的策略;
- 插入:INSERT_MARKUP,新的组件类型不在旧集合中,即全新的节点,需要对新节点进行插入操作。
- 删除:REMOVE_NODE,在新集合里也有,但对应的element不同则不能直接复用和更新,需要执行删除操作。或者旧组件不在新集合里的,也需要执行删除操作。
- 移动:MOVE_EXISTING,旧集合中有新组件类型,且element是可更新的类型,这时候就需要做移动操作,可以复用以前的DOM节点。
源码
创建vNode对象
const h = (tag, data = {}, children) => {
let text = ''
let el
let key
// 子元素是文本节点
if (typeof children === 'string' || typeof children === 'number') {
text = children
children = undefined
} else if (!Array.isArray(children)) {
children = undefined
}
if (data && data.key) {
key = data.key
}
return {
tag, // 元素标签
children, // 子元素
text, // 文本节点的文本
el, // 真实dom
key,
data
}
}
打补丁
patch函数是我们的主函数,主要用来进行新旧同级VNode的对比,找到差异来更新实际DOM,它接收两个参数,第一个参数可以是DOM元素或者是VNode,表示旧的VNode,第二参数表示新的VNode,一般只有第一次调用时才会传DOM元素,如果第一个参数为DOM元素的话我们直接忽略它的子元素把它转为一个VNode
-
元素标签相同
-
元素类型相同,那么新元素可以复用旧元素的dom节点
-
执行class,style,事件的绑定等
-
节点操作
- 新节点的子节点是文本节点,那么就直接替换
- 新旧节点都存在子节点,那么就要进行diff
- 新节点存在子节点,旧节点不存在
- 新节点不存在子节点,那么移除旧节点的所有子节点
- 新节点啥也没有,旧节点存在文本节点
-
-
标签不同,根据新的VNode创建新的dom节点,然后插入新节点,移除旧节点
//打补丁,针对同级的节点处理
const patchVNode = (oldVNode, newVNode) => {
console.log('patchVNode', oldVNode, newVNode);
if (oldVNode === newVNode) {
return
}
// 元素标签相同
if (oldVNode.tag === newVNode.tag) {
// 元素类型相同,那么新元素可以复用旧元素的dom节点
newVNode.el = oldVNode.el;
let el = newVNode.el;
console.log(oldVNode, newVNode);
handleStyle.updateClass(el, newVNode)
handleStyle.updateStyle(el, oldVNode, newVNode)
handleStyle.updateAttr(el, oldVNode, newVNode)
handleEvent.updateEvent(el, oldVNode, newVNode)
// 新节点的子节点是文本节点,那么就直接替换
if (newVNode.text) {
// 移除旧节点的子节点
if (oldVNode.children) {
console.log(oldVNode.children);
oldVNode.children.forEach((item) => {
console.log(item);
handleEvent.removeEvent(item)
})
}
// 文本内容不相同则更新文本
if (oldVNode.text !== newVNode.text) {
el.textContent = newVNode.text
}
} else {
// 新旧节点都存在子节点,那么就要进行diff
if (oldVNode.children && newVNode.children) {
diff(el, oldVNode.children, newVNode.children)
} else if (newVNode.children) { // 新节点存在子节点,旧节点不存在
// 旧节点存在文本节点则移除
if (oldVNode.text) {
el.textContent = ''
}
// 添加新节点的子节点
newVNode.children.forEach((item, index) => {
el.appendChild(createEl(newVNode.children[index]))
})
} else if (oldVNode.children) { // 新节点不存在子节点,那么移除旧节点的所有子节点
oldVNode.children.forEach((item) => {
handleEvent.removeEvent(item)
el.removeChild(item.el)
})
} else if (oldVNode.text) { // 新节点啥也没有,旧节点存在文本节点
el.textContent = ''
}
}
} else { // 标签不同,根据新的VNode创建新的dom节点,然后插入新节点,移除旧节点
let newEl = createEl(newVNode)
updateClass(newEl, newVNode)
updateStyle(newEl, null, newVNode)
updateAttr(newEl, null, newVNode)
handleEvent.removeEvent(oldNode)
updateEvent(newEl, null, newVNode)
let parent = oldVNode.el.parentNode
parent.insertBefore(newEl, oldVNode.el)
parent.removeChild(oldVNode.el)
}
}
//入口方法
const patch = (oldVNode, newVNode) => {
console.log('patch', oldVNode, oldVNode.tag, oldVNode.tagName, 'oldVNode');
// 初始化的时候,dom元素转换成vnode
if (!oldVNode.tag) {
let el = oldVNode
el.innerHTML = ''
oldVNode = h(oldVNode.tagName.toLowerCase())
oldVNode.el = el
}
patchVNode(oldVNode, newVNode)
return newVNode
}
diff函数
上述四个位置的排列组合:oldStartIdx与newStartIdx、oldEndIdx与newEndIdx,每当发现所比较的两个节点可能可以复用的话,那么就对这两个节点进行patch和相应操作,并更新指针进入下一轮比较,那怎么判断两个节点是否能复用呢?这就需要使用到key了
-
比较首尾节点:Vue 从新旧虚拟 DOM 树的头和尾进行双端比较:
-
相同
- 旧头-新头:只更新指针虚拟节点和指针
- 旧尾-新尾:只更新指针虚拟节点和指针
- 旧头-新尾:操作dom把旧指针节点移动到后面,并更新指针虚拟节点和指针
- 旧尾-新头:操作dom把旧指针节点移动到前面,并更新指针虚拟节点和指针
-
不同:通过遍历找到中间虚拟节点是否有可复用的
- 复用:操作dom把旧指针节点移动到前面,并更新指针虚拟节点和指针,并将对应旧虚拟节点置为null
- 不复用:根据vdom创建dom并移动到前面
-
-
旧列表里存在新列表里没有的节点,需要删除
const diff = (el, oldChildren, newChildren) => {
console.log('diff');
// 指针
let oldStartIdx = 0
let oldEndIdx = oldChildren.length - 1
let newStartIdx = 0
let newEndIdx = newChildren.length - 1
// 节点
let oldStartVNode = oldChildren[oldStartIdx]
let oldEndVNode = oldChildren[oldEndIdx]
let newStartVNode = newChildren[newStartIdx]
let newEndVNode = newChildren[newEndIdx]
//子节点个数都要大于1
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVNode === null) {
oldStartVNode = oldChildren[++oldStartIdx]
} else if (oldEndVNode === null) {
oldEndVNode = oldChildren[--oldEndIdx]
} else if (newStartVNode === null) {
newStartVNode = oldChildren[++newStartIdx]
} else if (newEndVNode === null) {
newEndVNode = oldChildren[--newEndIdx]
} else if (isSameNode(oldStartVNode, newStartVNode)) { // 头-头
console.log('头-头', oldStartVNode, newStartVNode);
// 更新指针
oldStartVNode = oldChildren[++oldStartIdx]
newStartVNode = newChildren[++newStartIdx]
} else if (isSameNode(oldStartVNode, newEndVNode)) { // 头-尾
console.log('头-尾');
patchVNode(oldStartVNode, newEndVNode)
// 把oldStartVNode节点移动到最后
el.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling)
// 更新指针
oldStartVNode = oldChildren[++oldStartIdx]
newEndVNode = newChildren[--newEndIdx]
} else if (isSameNode(oldEndVNode, newStartVNode)) { // 尾-头
console.log('尾-头');
patchVNode(oldEndVNode, newStartVNode)
// 把oldEndVNode节点移动到oldStartVNode前
el.insertBefore(oldEndVNode.el, oldStartVNode.el)
// 更新指针
oldEndVNode = oldChildren[--oldEndIdx]
newStartVNode = newChildren[++newStartIdx]
} else if (isSameNode(oldEndVNode, newEndVNode)) { // 尾-尾
console.log('尾-尾');
patchVNode(oldEndVNode, newEndVNode)
// 更新指针
oldEndVNode = oldChildren[--oldEndIdx]
newEndVNode = newChildren[--newEndIdx]
} else {
console.log('insertBefore');
let findIndex = findSameNode(oldChildren, newStartVNode)
// newStartVNode在旧列表里不存在,那么是新节点,创建插入
if (findIndex === -1) {
el.insertBefore(createEl(newStartVNode), oldStartVNode.el)
} else { // 在旧列表里存在,那么进行patch,并且移动到oldStartVNode前
let oldVNode = oldChildren[findIndex]
patchVNode(oldVNode, newStartVNode)
el.insertBefore(oldVNode.el, oldStartVNode.el)
oldChildren[findIndex] = null
}
newStartVNode = newChildren[++newStartIdx]
}
}
// 旧列表里存在新列表里没有的节点,需要删除
if (oldStartIdx <= oldEndIdx) {
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
handleEvent.removeEvent(oldChildren[i])
oldChildren[i] && el.removeChild(oldChildren[i].el)
}
} else if (newStartIdx <= newEndIdx) {
let before = newChildren[newEndIdx + 1] ? newChildren[newEndIdx + 1].el : null
for (let i = newStartIdx; i <= newEndIdx; i++) {
el.insertBefore(createEl(newChildren[i]), before)
}
}
}
处理函数
处理样式
//处理样式
const handleStyle = {
updateClass: (el, newVNode) => {
el.className = ''
if (newVNode.data && newVNode.data.class) {
let className = ''
Object.keys(newVNode.data.class).forEach((cla) => {
if (newVNode.data.class[cla]) {
className += cla + ' '
}
})
el.className = className
}
},
updateStyle: (el, oldVNode, newVNode) => {
let oldStyle = oldVNode && oldVNode.data && oldVNode.data.style || {}
let newStyle = newVNode && newVNode.data && newVNode.data.style || {}
// 移除旧节点里存在新节点里不存在的样式
Object.keys(oldStyle).forEach((item) => {
if (newStyle[item] === undefined || newStyle[item] === '') {
el.style[item] = ''
}
})
// 添加旧节点不存在的新样式
Object.keys(newStyle).forEach((item) => {
if (oldStyle[item] !== newStyle[item]) {
el.style[item] = newStyle[item]
}
})
},
updateAttr: (el, oldVNode, newVNode) => {
let oldAttr = oldVNode && oldVNode.data && oldVNode.data.attr ? oldVNode.data.attr : {}
let newAttr = newVNode && newVNode.data && newVNode.data.attr || {}
// 移除旧节点里存在新节点里不存在的属性
Object.keys(oldAttr).forEach((item) => {
if (newAttr[item] === undefined || newAttr[item] === '') {
el.removeAttribute(item)
}
})
// 添加旧节点不存在的新属性
Object.keys(newAttr).forEach((item) => {
if (oldAttr[item] !== newAttr[item]) {
el.setAttribute(item, newAttr[item])
}
})
}
}
处理事件
//处理事件
const handleEvent = {
removeEvent: (oldVNode) => {
console.log();
if (oldVNode && oldVNode.data && oldVNode.data.event) {
Object.keys(oldVNode.data.event).forEach((item) => {
oldVNode.el.removeEventListener(item, oldVNode.data.event[item])
})
}
},
updateEvent: (el, oldVNode, newVNode) => {
let oldEvent = oldVNode && oldVNode.data && oldVNode.data.event ? oldVNode.data.event : {}
let newEvent = newVNode && newVNode.data && newVNode.data.event || {}
// 移除旧节点里存在新节点里不存在的事件
Object.keys(oldEvent).forEach((item) => {
if (newEvent[item] === undefined || oldEvent[item] !== newEvent[item]) {
el.removeEventListener(item, oldEvent[item])
}
})
// 添加旧节点不存在的新事件
Object.keys(newEvent).forEach((item) => {
if (oldEvent[item] !== newEvent[item]) {
el.addEventListener(item, newEvent[item])
}
})
}
}
渲染dom
//渲染dom
const createEl = (vnode) => {
console.log(vnode);
let el = document.createElement(vnode.tag)
vnode.el = el;
if (Array.isArray(vnode) && vnode.children && vnode.children.length > 0) {
vnode.children.forEach((item) => {
el.appendChild(createEl(item))
})
} else {//重点:初始化的时候需要给虚拟节点赋值text,children。下次比较新旧节点的时候才是正确的。这里需要优化
vnode.text = vnode.children
vnode.children = undefined
}
if (vnode.text) {
el.appendChild(document.createTextNode(vnode.text))
}
console.log('createEl:vnode', vnode);
return el
}
调用
//调用
let preVNode = patch(
document.getElementById("app"),
h(
"div",
{
class: {
btn: true,
},
style: {
fontSize: "30px",
},
attr: {
id: "oldId",
},
event: {
mouseover: () => {
setTimeout(() => {
let newVNode = h(
"div",
{
class: {//类名改变
btn: true,
warning: false,
bg: true,
},
style: {//样式改变
fontWeight: "bold",
fontStyle: "italic",
},
attr: {
id: "newId",//id改变
},
event: {
click: () => {
alert("点了我");
},
},
},
[//reorder 移动/增加/删除 子节点
{
tag: 'h1',
children: '已经移入'//text 文本变了 此时不会触发节点卸载和装载,而是节点更新
},
{
tag: 'h3',//replace 节点类型变了 直接将旧节点卸载并装载新节点
children: 'item3'
},
]
);
console.log('preVNode:', preVNode, 'newVNode:', newVNode);
patch(preVNode, newVNode);
}, 1000);
},
},
},
[
{
tag: 'h1',
children: '移入我'
},
{
tag: 'h2',
children: 'item1'
},
{
tag: 'h2',
children: 'item2'
}
]
)
);
实现代码
//创建vNode对象
const h = (tag, data = {}, children) => {
let text = ''
let el
let key
// 文本节点
if (typeof children === 'string' || typeof children === 'number') {
text = children
children = undefined
} else if (!Array.isArray(children)) {
children = undefined
}
if (data && data.key) {
key = data.key
}
return {
tag, // 元素标签
children, // 子元素
text, // 文本节点的文本
el, // 真实dom
key,
data //样式,class类,事件等
}
}
//处理样式
const handleStyle = {
updateClass: (el, newVNode) => {
el.className = ''
if (newVNode.data && newVNode.data.class) {
let className = ''
Object.keys(newVNode.data.class).forEach((cla) => {
if (newVNode.data.class[cla]) {
className += cla + ' '
}
})
el.className = className
}
},
updateStyle: (el, oldVNode, newVNode) => {
let oldStyle = oldVNode && oldVNode.data && oldVNode.data.style || {}
let newStyle = newVNode && newVNode.data && newVNode.data.style || {}
// 移除旧节点里存在新节点里不存在的样式
Object.keys(oldStyle).forEach((item) => {
if (newStyle[item] === undefined || newStyle[item] === '') {
el.style[item] = ''
}
})
// 添加旧节点不存在的新样式
Object.keys(newStyle).forEach((item) => {
if (oldStyle[item] !== newStyle[item]) {
el.style[item] = newStyle[item]
}
})
},
updateAttr: (el, oldVNode, newVNode) => {
let oldAttr = oldVNode && oldVNode.data && oldVNode.data.attr ? oldVNode.data.attr : {}
let newAttr = newVNode && newVNode.data && newVNode.data.attr || {}
// 移除旧节点里存在新节点里不存在的属性
Object.keys(oldAttr).forEach((item) => {
if (newAttr[item] === undefined || newAttr[item] === '') {
el.removeAttribute(item)
}
})
// 添加旧节点不存在的新属性
Object.keys(newAttr).forEach((item) => {
if (oldAttr[item] !== newAttr[item]) {
el.setAttribute(item, newAttr[item])
}
})
}
}
//处理事件
const handleEvent = {
removeEvent: (oldVNode) => {
console.log();
if (oldVNode && oldVNode.data && oldVNode.data.event) {
Object.keys(oldVNode.data.event).forEach((item) => {
oldVNode.el.removeEventListener(item, oldVNode.data.event[item])
})
}
},
updateEvent: (el, oldVNode, newVNode) => {
let oldEvent = oldVNode && oldVNode.data && oldVNode.data.event ? oldVNode.data.event : {}
let newEvent = newVNode && newVNode.data && newVNode.data.event || {}
// 移除旧节点里存在新节点里不存在的事件
Object.keys(oldEvent).forEach((item) => {
if (newEvent[item] === undefined || oldEvent[item] !== newEvent[item]) {
el.removeEventListener(item, oldEvent[item])
}
})
// 添加旧节点不存在的新事件
Object.keys(newEvent).forEach((item) => {
if (oldEvent[item] !== newEvent[item]) {
el.addEventListener(item, newEvent[item])
}
})
}
}
//渲染dom
const createEl = (vnode) => {
console.log(vnode);
let el = document.createElement(vnode.tag)
vnode.el = el;
if (Array.isArray(vnode) && vnode.children && vnode.children.length > 0) {
vnode.children.forEach((item) => {
el.appendChild(createEl(item))
})
} else {
vnode.text = vnode.children
vnode.children = undefined
}
if (vnode.text) {
el.appendChild(document.createTextNode(vnode.text))
}
console.log('createEl:vnode', vnode);
return el
}
//判断是否是同一个节点
const isSameNode = (a, b) => {
return a.key === b.key && a.tag === b.tag
}
//在节点列表里寻找同个节点,返回索引
const findSameNode = (list, node) => {
return list.findIndex((item) => {
return item && isSameNode(item, node)
})
}
//diff算法
const diff = (el, oldChildren, newChildren) => {
console.log('diff');
// 指针
let oldStartIdx = 0
let oldEndIdx = oldChildren.length - 1
let newStartIdx = 0
let newEndIdx = newChildren.length - 1
// 节点
let oldStartVNode = oldChildren[oldStartIdx]
let oldEndVNode = oldChildren[oldEndIdx]
let newStartVNode = newChildren[newStartIdx]
let newEndVNode = newChildren[newEndIdx]
//子节点个数都要大于1
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVNode === null) {
oldStartVNode = oldChildren[++oldStartIdx]
} else if (oldEndVNode === null) {
oldEndVNode = oldChildren[--oldEndIdx]
} else if (newStartVNode === null) {
newStartVNode = oldChildren[++newStartIdx]
} else if (newEndVNode === null) {
newEndVNode = oldChildren[--newEndIdx]
} else if (isSameNode(oldStartVNode, newStartVNode)) { // 头-头
console.log('头-头', oldStartVNode, newStartVNode);
// 更新指针
oldStartVNode = oldChildren[++oldStartIdx]
newStartVNode = newChildren[++newStartIdx]
} else if (isSameNode(oldStartVNode, newEndVNode)) { // 头-尾
console.log('头-尾');
patchVNode(oldStartVNode, newEndVNode)
// 把oldStartVNode节点移动到最后
el.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling)
// 更新指针
oldStartVNode = oldChildren[++oldStartIdx]
newEndVNode = newChildren[--newEndIdx]
} else if (isSameNode(oldEndVNode, newStartVNode)) { // 尾-头
console.log('尾-头');
patchVNode(oldEndVNode, newStartVNode)
// 把oldEndVNode节点移动到oldStartVNode前
el.insertBefore(oldEndVNode.el, oldStartVNode.el)
// 更新指针
oldEndVNode = oldChildren[--oldEndIdx]
newStartVNode = newChildren[++newStartIdx]
} else if (isSameNode(oldEndVNode, newEndVNode)) { // 尾-尾
console.log('尾-尾');
patchVNode(oldEndVNode, newEndVNode)
// 更新指针
oldEndVNode = oldChildren[--oldEndIdx]
newEndVNode = newChildren[--newEndIdx]
} else {
console.log('insertBefore');
let findIndex = findSameNode(oldChildren, newStartVNode)
// newStartVNode在旧列表里不存在,那么是新节点,创建插入
if (findIndex === -1) {
el.insertBefore(createEl(newStartVNode), oldStartVNode.el)
} else { // 在旧列表里存在,那么进行patch,并且移动到oldStartVNode前
let oldVNode = oldChildren[findIndex]
patchVNode(oldVNode, newStartVNode)
el.insertBefore(oldVNode.el, oldStartVNode.el)
oldChildren[findIndex] = null
}
newStartVNode = newChildren[++newStartIdx]
}
}
// 旧列表里存在新列表里没有的节点,需要删除
if (oldStartIdx <= oldEndIdx) {
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
handleEvent.removeEvent(oldChildren[i])
oldChildren[i] && el.removeChild(oldChildren[i].el)
}
} else if (newStartIdx <= newEndIdx) {
let before = newChildren[newEndIdx + 1] ? newChildren[newEndIdx + 1].el : null
for (let i = newStartIdx; i <= newEndIdx; i++) {
el.insertBefore(createEl(newChildren[i]), before)
}
}
}
//打补丁,针对同级的节点处理
const patchVNode = (oldVNode, newVNode) => {
console.log('patchVNode', oldVNode, newVNode);
if (oldVNode === newVNode) {
return
}
// 元素标签相同
if (oldVNode.tag === newVNode.tag) {
// 元素类型相同,那么新元素可以复用旧元素的dom节点
newVNode.el = oldVNode.el;
let el = newVNode.el;
console.log(oldVNode, newVNode);
handleStyle.updateClass(el, newVNode)
handleStyle.updateStyle(el, oldVNode, newVNode)
handleStyle.updateAttr(el, oldVNode, newVNode)
handleEvent.updateEvent(el, oldVNode, newVNode)
// 新节点的子节点是文本节点,那么就直接替换
if (newVNode.text) {
// 移除旧节点的子节点
if (oldVNode.children) {
console.log(oldVNode.children);
oldVNode.children.forEach((item) => {
console.log(item);
handleEvent.removeEvent(item)
})
}
// 文本内容不相同则更新文本
if (oldVNode.text !== newVNode.text) {
el.textContent = newVNode.text
}
} else {
// 新旧节点都存在子节点,那么就要进行diff
if (oldVNode.children && newVNode.children) {
diff(el, oldVNode.children, newVNode.children)
} else if (newVNode.children) { // 新节点存在子节点,旧节点不存在
// 旧节点存在文本节点则移除
if (oldVNode.text) {
el.textContent = ''
}
// 添加新节点的子节点
newVNode.children.forEach((item, index) => {
el.appendChild(createEl(newVNode.children[index]))
})
} else if (oldVNode.children) { // 新节点不存在子节点,那么移除旧节点的所有子节点
oldVNode.children.forEach((item) => {
handleEvent.removeEvent(item)
el.removeChild(item.el)
})
} else if (oldVNode.text) { // 新节点啥也没有,旧节点存在文本节点
el.textContent = ''
}
}
} else { // 标签不同,根据新的VNode创建新的dom节点,然后插入新节点,移除旧节点
let newEl = createEl(newVNode)
updateClass(newEl, newVNode)
updateStyle(newEl, null, newVNode)
updateAttr(newEl, null, newVNode)
handleEvent.removeEvent(oldNode)
updateEvent(newEl, null, newVNode)
let parent = oldVNode.el.parentNode
parent.insertBefore(newEl, oldVNode.el)
parent.removeChild(oldVNode.el)
}
}
//入口方法
const patch = (oldVNode, newVNode) => {
console.log('patch', oldVNode, oldVNode.tag, oldVNode.tagName, 'oldVNode');
// 初始化的时候,dom元素转换成vnode
if (!oldVNode.tag) {
let el = oldVNode
el.innerHTML = ''
oldVNode = h(oldVNode.tagName.toLowerCase())
oldVNode.el = el
}
patchVNode(oldVNode, newVNode)
return newVNode
}