解析Vue2源码中的diff算法
1.patch
在Vue中,把DOM-diff过程叫做patch过程,其本质就是对比新旧VNode。所谓旧的VNode就是在数据变化前的虚拟DOM节点,新的即是数据变化后需要渲染的新的视图所对应的虚拟DOM节点。过程差不多就是以新的VNode为基准,对比旧VNode,如果新的有的节点旧的没有,就添加上去;如果新的没有的节点而旧的有的,就将其删除,如果都有但是内容不同的节点,就将其更新。以此让新旧相同。
总结下来就是三个操作:
- 创建节点:新的
VNode
中有而旧的oldVNode
中没有,就在旧的oldVNode
中创建。 - 删除节点:新的
VNode
中没有而旧的oldVNode
中有,就从旧的oldVNode
中删除。 - 更新节点:新的
VNode
和旧的oldVNode
中都有,就以新的VNode
为准,更新旧的oldVNode
。
这便是vue的patch过程。接下来根据源码逐个分析如何将其实现。
2.创建节点
上节解析过VNode可以描述的六种类型的节点,但实际上只有3种类型的节点能够被创建并插入到DOM中,他们是:元素节点,文本节点,注释节点。所以vue在创建节点的时候会判断在新的VNode中有的而旧的没有的节点是属于哪种类型的节点,从而调用不同的方法创建并插入到DOM中。
// 源码位置: /src/core/vdom/patch.js
function createElm (vnode, parentElm, refElm) {
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
vnode.elm = nodeOps.createElement(tag, vnode) // 创建元素节点
createChildren(vnode, children, insertedVnodeQueue) // 创建元素节点的子节点
insert(parentElm, vnode.elm, refElm) // 插入到DOM中
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text) // 创建注释节点
insert(parentElm, vnode.elm, refElm) // 插入到DOM中
} else {
vnode.elm = nodeOps.createTextNode(vnode.text) // 创建文本节点
insert(parentElm, vnode.elm, refElm) // 插入到DOM中
}
}
从上面的代码我们可以看出:
1.判断是否为元素节点只看它有没有tag标签,如果有则视为元素节点,则调用createElement方法创建元素节点,通常元素节点还会有子节点,就递归遍历创建所有子节点,将所有创建好后insert插入到当前元素节点,最后把该元素节点插入DOM中。
2.判断是否为注释节点,只需判断VNode的isComment属性是否为true即可,若是则为注释节点,则调用createComment方法创建注释节点,在插入。
3.如果以上都不是,那就是文本节点,则调用createTextNode方法创建文本节点,再插入。
[!NOTE]
代码中的nodeOps是Vue为了跨平台兼容性,对所有节点操作进行了封装,例如nodeOps.createTextNode()在浏览器端等于document.createTextNode()
3.删除节点
vue写了removeChild方法
function removeNode (el) {
const parent = nodeOps.parentNode(el) // 获取父节点
if (isDef(parent)) {
nodeOps.removeChild(parent, el) // 调用父节点的removeChild方法
}
}
4.更新节点
更新节点相对于上面两个来说相对复杂了,当某些节点在新旧都有时,就需要细致比较一下找出不一样的地方进行更新。
讲逻辑之前先简单介绍一下静态节点看个例子:
<p>刘畏寒是大莎币</p>
上面这个节点只包含了纯文字,没有任何的变量,也就是说无论数据怎么变化,这个节点永远不会改变,这种就叫做静态节点。
有了这个概念后我们就可以开始更新节点,我们需要对三种情况进行判断并处理。
1.如果新旧VNode均为静态节点。
直接跳过,因为它不包含任何数据,永远不会发生变化。
2.如果VNode是文本节点
表示这个节点内只包含纯文本,那么看旧节点是否为文本节点,如果是则比较文本是否相同,不相同则修改为相同文本;如果旧节点不是文本节点,那么不管他是什么,直接调用生成文本节点的方法把它改成文本节点且内容相同。
3.如果VNode是元素节点
3.1该节点包含子节点
如果新的节点内包含子节点,那么此时要看旧的节点是否包含子节点,如果旧的也包含了子节点,那就需要递归对比子节点。如果旧节点不包含子节点,那么这个旧节点有可能是文本节点或者空节点,如果是文本节点就清空文本内容然后把新的子节点创建一份插入旧节点,如果旧节点是空节点,直接插入子节点。
3.2该节点不包含子节点
如果不包含子节点同时又不是文本节点,那就是空节点,直接清空旧节点就行了
// 更新节点
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// vnode与oldVnode是否完全一样?若是,退出程序
if (oldVnode === vnode) {
return
}
const elm = vnode.elm = oldVnode.elm
// vnode与oldVnode是否都是静态节点?若是,退出程序
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
return
}
const oldCh = oldVnode.children
const ch = vnode.children
// vnode有text属性?若没有:
if (isUndef(vnode.text)) {
// vnode的子节点与oldVnode的子节点是否都存在?
if (isDef(oldCh) && isDef(ch)) {
// 若都存在,判断子节点是否相同,不同则更新子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
}
// 若只有vnode的子节点存在
else if (isDef(ch)) {
/**
* 判断oldVnode是否有文本?
* 若没有,则把vnode的子节点添加到真实DOM中
* 若有,则清空Dom中的文本,再把vnode的子节点添加到真实DOM中
*/
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
}
// 若只有oldnode的子节点存在
else if (isDef(oldCh)) {
// 清空DOM中的子节点
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
}
// 若vnode和oldnode都没有子节点,但是oldnode中有文本
else if (isDef(oldVnode.text)) {
// 清空oldnode文本
nodeOps.setTextContent(elm, '')
}
// 上面两个判断一句话概括就是,如果vnode中既没有text,也没有子节点,那么对应的oldnode中有什么就清空什么
}
// 若有,vnode的text属性与oldVnode的text属性是否相同?
else if (oldVnode.text !== vnode.text) {
// 若相同:用vnode的text替换真实DOM的文本
nodeOps.setTextContent(elm, vnode.text)
}
}
上面便是源码如何实现这个逻辑。