什么是虚拟DOM?
虚拟dom即Virtual DOM 本质上是一个js对象,用来描述真实的dom节点,vue中实现了一个vnode类,用来表示虚拟节点。
源码路径:\src\core\vdom\vnode.js
export default class VNode {
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
/*当前节点的标签名*/
this.tag = tag
/* 包含当前节点的一些数据信息,类型是vnodeData,详情参考vnodeData中的数据信息 */
this.data = data
/* 当前节点的子节点,是个数组 */
this.children = children
/* 当前节点的文本 */
this.text = text
/* 当前虚拟节点的真实dom节点 */
this.elm = elm
/* 当前节点的命名空间 */
this.ns = undefined
/* 编译作用域 */
this.context = context
/* 函数组件作用域 */
this.fnContext = undefined
/* 函数组件的option选项 */
this.fnOptions = undefined
this.fnScopeId = undefined
/* 节点的key属性,作为节点的标识,patch时用来优化 */
this.key = data && data.key
/* 组件的option选项 */
this.componentOptions = componentOptions
/* 当前节点对应的组件实例 */
this.componentInstance = undefined
/* 当前节点的父节点 */
this.parent = undefined
/*是否为原生HTML或普通文本,innerHTML的时候为true,textContent的时候为false*/
this.raw = false
/*静态节点的标志*/
this.isStatic = false
/*是否作为根节点插入*/
this.isRootInsert = true
/*是否是注释节点*/
this.isComment = false
/*是否是克隆节点*/
this.isCloned = false
/*是否有v-once指令*/
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}
这里我移除了一些源码对参数类型的校验。
为什么使用虚拟DOM?
因为DOM操作的执行速度远不如JavaScript的运算速度快,所以将大量的DOM操作转变为JavaScript运算,使用diff算法来计算出真正需要更新的节点,最大限度的减少DOM操作,从而达到性能的提升。虚拟DOM最核心的部分是patch,它可以将vnode渲染成真实的DOM,所以这里我们主要来看vue中patch的过程。
patch过程
patch的本质是将新旧vnode进行比较,创建、删除或者更新DOM节点/组件实例。对比两个vnode之间的差异只是patch的一部分。patch的目的其实是修改DOM节点,也可以理解为渲染视图。vue中主要通过以下三个方法实现:pathch、pa,chVnode和updateChildren ,其中我们常提到的diff过程在updateChildren方法里,现在我们重点看一下这三个方法。
源码路径:\src\core\vdom\patch.js
patch方法
这里先来看一下patch方法的过程。
源码比较多,这里我就不贴了,有兴趣的小伙伴自己clone一份源码结合着看一看。
上面流程图提到当oldVnode和vnode“相同”时,会调用patchVnode进行详细对比,这里顺带提一嘴,vue中判断两个vnode是否“相同”,调用的是sameVnode方法,该方法会去比较两个vnode的key是否相等并且tag是否相等、和其他判断条件。这也是为什么v-for渲染元素列表时,需要给一个唯一值key。
patchVnode方法
这是patchVnode方法的整体流程,建议结合源码使用更佳。 上图提到当oldVnode和vnode都存在子节点且不相等时,调用updateChildren方法更新子节点,敲黑板,到这里才是真正的重点。
updateChildren方法
我们来看下updateChildren方法,虽然看起来挺唬人的,但是建议静下心来过一遍,再结合后面的图看一下,相信聪明的你是能够理解的。
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
我们可以看到程序一开始会在oldChildren、newChildren的头尾各创建一个指针,然后开始遍历,而整个循环的条件是oldStartIdx >= oldEndIdx并且newStartIdx >= newEndIdx,如果有一个条件不满足,说明有一个数组已经遍历完了,循环体内则进行各种情况的判断。
1、旧头和新头相同时
当旧头和新头相同时,递归调用patchVnode更新节点,然后将oldStartIdx、newStartIdx向后移,节点位置不需要移动。
2、旧尾和新尾相同时
当旧尾和旧头相同时,递归调用patchVnode更新节点,然后将oldEndIdx、newEndIdx向前移,节点位置不需要移动。
3、旧头和新尾相同时
当旧头和新尾相同时,递归调用patchVnode更新节点,然后再调用insertBefore钩子将oldStartVnode.elm(对应的真实DOM节点)移动到oldEndVnode.elm的后面,web平台对应方法是insertBefore。最后将oldStartIdx后移一位、newEndIdx前移一位。
4、旧尾和新头相同时
当旧尾和新头相同时,递归调用patchVnode更新节点,然后再调用insertBefore钩子将oldEndVnode.elm移动到oldStartVnode.elm的前面,最后将oldEndIdx前移一位、newStartIdx后移一位。
5、不满足以上条件的
当出现这种不满足以上条件的,会用oldChildren创建一个哈希表,其中vnode的key作为键,下标作为值。如果newStartVnode.key存在就会在哈希表中查找,不存在的话就遍历oldChildren查找。如果没有找到对应的,则用newStartVnode创建一个新的真实节点放到oldStartVnode.elm的前面,找到对应的则调用sameVnode判断新旧是否相同,相同调用patchVnode更新节点并移动该节点到oldStartVnode.elm的前面,不相同则新建节点。最后将newStartIdx后移一位。
到这里我们的循环体内的判断结束了,但是如果oldChildren和newChildren长度不一样,则其中一个循环结束了,另一个还会有剩余节点。所以接下来我们要对这部分进行判断。
如果oldChildren先循环结束,newChildren还有剩余节点,此时oldStartIdx必然大于oldEndIdx,我们将newStartIdx于newEndIdx之间的vnode创建真实节点添加到视图中即可。同理,当newChildren先循环结束,oldChildren还有剩余节点时 ,此时newStartIdx必然大于newEndIdx,我们移除掉oldStartIdx与oldEndIdx之间的节点即可。
为什么要设置key属性以及为什么不建议用数组的index作为key?
因为key会作为节点的唯一id,更新子节点时,如果不存在key会遍历查找oldChildren,如果设置了key则会去哈希表中查找,会提升一定的性能。那为什么不建议使用index作为key呢?这里我们要去看sameVnode方法,它在判断两个vnode是否相同时主要就是去比较key和tag。当我们使用index作为key时,例如:oldChildrenKeyArr: 0 , 1 , 2 , 3 如果我们删除了数组的第一项,按道理来说我们只需要移除第一个节点就可以了。但是实际上删除之后是这样的newChildrenKeyArr: 0 , 1 , 2 再调用updateChildren更新子节点,遍历结束反而删除的是最后一项,造成性能的浪费。如果使用id(唯一值)就不会出现这种情况。
写在最后
至此,vue的虚拟DOM就结束了,希望大家从这篇文章中能有所收获。
灵魂拷问: 你学废了吗?(手动狗头)
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!