VUE中的DOM-Diff
前言
VNode最大的用途就是在数据变化前后生成真实DOM对应的虚拟DOM节点,然后就可以对比新旧两份VNode,找出差异所在,然后更新有差异的DOM节点,最终达到以最少操作真实DOM更新视图的目的。而对比新旧两份VNode并找出差异的过程就是所谓的DOM-Diff过程。DOM-Diff算法是整个虚拟DOM的核心所在。
patch
在Vue中,把DOM-Diff过程叫做patch过程,patch意味“补丁”,即对旧的VNode修补,打补丁从而得到新的VNode,所谓旧的VNode(即oldVNode)就是数据变化之前的视图所对应的虚拟DOM节点,而新的VNode是数据变化之后将要渲染的新的视图所对应的虚拟DOM节点,所以,我们要以生成的新的VNode为基准,对比旧的oldVNode,如果新的VNode上有的节点而旧的oldVNode上没有,那么就在旧的oldVNode上加上去,如果新的VNode上没有而旧的oldVNode上有,那么就在旧的oldVNode上去掉,如果某些节点在新的VNode和旧的VNode上都有,那么就以新的VNode为准,更新旧的oldVNode,从而让新旧VNode相同。
总而言之就一句话,以新的VNode为基准,改造旧的oldVNode使之称为跟新的VNode一样,这就是patch过程要干的事
整个patch只干了三件事:
- 创建节点:新的VNode中有而旧的oldVNode中没有,就在旧的oldVNode中创建。
- 删除节点:新的VNode中没有而旧的oldVNode中有,就在旧的oldVNode中删除。
- 更新节点:新的VNode和旧的oldVNode中都有,就以新的VNode为准,更新旧的oldVNode。
创建节点
VNode类可以描述6中类型的节点,但实际上只有三种类型的节点能够被创建并插入到DOM中,它们分别是元素节点、注释节点、文本节点。所以VUE在创建节点的时候会判断在新的VNode中有而旧的oldVNode中没有的这个节点属于那种类型,从而调用不同的方法创建并插入到DOM中。
//源码位置: /src/core/vdom/patch.js
function createElm (vnode,parentElm,refElm){
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
is(isDef(tag)){ //如果有tag就是元素节点,isDef()方法是判断传入的参数不为undefined和null
vnode.elm = nodeOps.createElement(tag,vnode) //创建元素节点
createChildren(vnode,children,insertedVnodeQueue) //创建元素节点的子节点
insert(parentElm,vnode.elm,refElm) //插入到DOM中
}else if(isTrue(vnode.isComment)){ //如果isComment为true就时注释节点
vnode.elm = nodeOps.createComment(vnode.text) //创建注释节点
insert(parentElm,vnode.elm,refElm) //插入到DOM中
}else{//文本节点
vnode.elm = nodeOps.createTextNode(vnode.text) //创建文本节点
inset(parentElm,vnode.elm,refElm) //插入到DOM中
}
}
从上面的代码中可以看出:
- 判断是否为元素节点只需判断该VNode节点是否有tag标签即可。如果有tag属性即认为时元素节点,则调用createElement方法创建元素节点,通常元素节点还会有子节点,那就递归遍历创建所有子节点,将所有子节点创建之后insert插入到当前元素节点里面,最后把当前元素节点插入到DOM中。
- 判断是否为注释节点,只需判断VNode的isCommnet属性是否为true即可,若为true则为注释节点,则调用createComment方法创建注释节点,在出入到DOM中
- 如果既不是元素节点又不是注释节点,那就认为时文本节点,则调用createTextNode方法创建文本节点,再插入到DOM中
代码中nodeOPs时VUE为了跨平台兼容性,对所有节点操作进行封装,例如nodeOps.createTextNode()在浏览器端等同于document.createTextNode()
删除节点
如果某些节点在新的VNode中没有而在旧的oldVNode中有,那么就需要把这些节点从旧的oldVNode中删除。删除节点非常简单,只需要在删除节点的父元素上调用removeChild方法即可。
function removeNode(el){
const parent = nodeOps.parentNode(el) //获取父节点
if(isDef(parent)){
nodeOps.removeChild(parent,el) //调用父节点的removeChild方法
}
}
更新节点
创建节点和删除节点都比较简单,而更新节点相对复杂一点。
更新节点就是当某些节点在新的VNode和旧的VNode中都有时,我们就需要细致比较一下,找出不一样的地方进行更新。
在此之前我们需要知道什么时静态节点?
<p>我是不会变化的文字</p>
上面这个节点里面只包含了纯文字,没有任何可变的变量,这就是说,不管数据再怎么变化,只要这个节点第一次渲染了,那么他以后就永远不会发生变化,这是因为它不包含任何变量,所以数据发生任何变化都与它无关。我们把这种节点称之为静态节点。
OK,有了这个概念以后,我们开始更新节点。更新节点的时候我们需要对以下三种情况进行判断并反别处理:
- 如果VNode和oldVNode均为静态节点:可以直接跳过,无需处理
- 如果VNode是文本节点:如果VNode是文本节点即表示这个节点内只包含纯文本,那么只需看oldVnode是否也是文本节点,如果是,那就比较两个文本是否不同,如果不同则把oldVNode里面的文本改成跟VNode的文本一样,如果oldVNode不是文本节点,那么不论它是什么,直接调用setTextNode方法把它改成文本节点,并且文本内容跟VNode相同。
- 如果VNode是元素节点,会分成两种情况
- 该节点包含子节点
如果新的节点内包含子节点,那么此时要看旧的节点是否包含子节点,如果旧的节点里也包含子节点,那就需要递归对比更新节点;如果旧的节点里不包含子节点,那么这个就节点可能是空节点或者是文本节点,如果旧的节点是空节点就把新节点里的子节点创建一份然后插入到旧的节点里面,如果旧的节点是文本节点,则把文本清空,然后把新的节点里的子节点创建一份插入到旧的节点里面 - 该节点不包含子节点
如果该节点不包含子节点,同时它又不是文本节点,那就说明该节点是个空节点,那么直接清空就节点即可
- 该节点包含子节点
//更新节点
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)
}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)
}else if(isDef(oldVnode)){
//若只有oldnode的子节点存在,清空DOM中的子节点
removeVnode(elm,oldCh,9,oldCh.length - 1)
}else if(isDef(oldVnode.text)){
//若vnode和oldnode都没有子节点,但是oldnode中有文本,则清空oldnode文本
nodeOps.setTextContent(elm,'')
}
}else if(oldVnode.text !== vnode.text){
//若有text属性与oldnode的text属性是否相同?若不相同,则用vnode的text替换真是DOM文本
nodeOps.setTextContent(elm,vnode.text)
}
}