<深入浅出Vuejs>虚拟DOM_Patch()
虚拟DOM最核心的部分是Patch方法,它通过对比新旧两个Vnode之间有哪些不同,然后根据对比结果找出需要更新的节点进行更新.当oldVnode和Vnode不相同时,以Vnode为准来渲染视图.
而Patch中使用了Diff算法来进行比较.Diff 算法是一种通过同层的树节点进行比较的高效算法,避免了对树进行逐层搜索遍历,所以时间复杂度只有 O(n)
让我们先来看看VUE初始化时最后的工作:vm.$mount将vm实例挂载到DOM元素上.
Vue.prototype._init = function (options?: Object) {......if (vm.$options.el) {vm.$mount(vm.$options.el)}}
而vue.$mount()函数最后会调用lifestyle.js中的mountComponent方法:其中定义了一个updateComponent函数用于更新组件.
然后对这个Vue实例(组件)新建一个Watcher并完成依赖收集工作,当响应式数据发生变化时,它的dep数组便会调用dep.notify()来通知所有的依赖更新视图,此时就会调用updateComponent方法.
export function mountComponent (vm: Component,el: ?Element,hydrating?: boolean
): Component {//el代表DOM节点vm.$el = el......//钩子函数callHook(vm, 'beforeMount')let updateComponent/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {...}} else {updateComponent = () => {// vm._render() 返回一个VNode,在生成VNode的过程中,会动态计算getter,同时推入到dep里 vm._update(vm._render(), hydrating)}}new Watcher(vm, updateComponent, noop, {before () {if (vm._isMounted && !vm._isDestroyed) {callHook(vm, 'beforeUpdate')}}}, true /* isRenderWatcher */)hydrating = false// manually mounted instance, call mounted on self// mounted is called for render-created child components in its inserted hookif (vm.$vnode == null) {vm._isMounted = truecallHook(vm, 'mounted')}return vm
}
updateComponent(): updateComponent()执行的时候内部会调用,vm.__update()方法,并将_render()函数产生的新Vnode传入其中,与OldVnode做diff比较,最后完成更新工作.而_update()函数的定义如下:
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {const vm: Component = thisconst prevEl = vm.$elconst prevVnode = vm._vnodeconst restoreActiveInstance = setActiveInstance(vm)vm._vnode = vnode //由render产生的新的Vnode// Vue.prototype.__patch__ is injected in entry points// based on the rendering backend used.//如果不存在OldVnode,就使用新Vnode创建一个真实DOMif (!prevVnode) {// initial rendervm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)} else {// updates//如果存在OldVnode,就调用patch将OldVnode与Vnode做diff操作,将需要更新的Dom节点进行更新.vm.$el = vm.__patch__(prevVnode, vnode)}restoreActiveInstance()// update __vue__ referenceif (prevEl) {prevEl.__vue__ = null}if (vm.$el) {vm.$el.__vue__ = vm}// if parent is an HOC, update its $el as wellif (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {vm.$parent.$el = vm.$el}// updated hook is called by the scheduler to ensure that children are// updated in a parent's updated hook.}
__update:__update()中调用了__patch__函数.
综上,patch不是暴力替换节点,而是修改DOM节点(也可以理解为渲染视图),需要做三件事:
1.创建新增的节点 (发生于OldVnode不存在而Vnode存在时,或Vnode和oldVnode不是同一个节点)
2.删除废弃节点 (发生于节点只存在于OldVnode中)
3.修改需要更新的节点(当两个节点时相同的节点时,做Diff比较,更新不同的节点)
__patch__:那么vm.__patch__函数中究竟发生了什么呢?
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {// 当oldVnode不存在时if (isUndef(oldVnode)) {// 创建新的节点createElm(vnode, insertedVnodeQueue, parentElm, refElm)} else {const isRealElement = isDef(oldVnode.nodeType)if (!isRealElement && sameVnode(oldVnode, vnode)) {// 对oldVnode和vnode进行diff,并对oldVnode打patchpatchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)} }//sameNode
function sameVnode (a, b) {return (a.key === b.key &&a.asyncFactory === b.asyncFactory && ((a.tag === b.tag &&a.isComment === b.isComment &&isDef(a.data) === isDef(b.data) &&sameInputType(a, b)) || (isTrue(a.isAsyncPlaceholder) &&isUndef(b.asyncFactory.error))))
}
如果不存在oldVnode,就直接用新Vnode创建一个新节点,否则调用sameVnode函数判断OldVnode和Vnode是不是同一个节点(通过判断基本属性),如果是则调用Diff,否则直接跳过Diff过程,根据Vnode创建新的DOM节点,然后删除OldVnode的DOM节点.
patchVnode:我们着重分析更新节点过程,即Diff过程.对应的方法为patchVnode()(Patch.js)
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {// 如果 oldVnode、vnode 是同一节点的话,则直接 return 即可,不同进行更新操作if (oldVnode === vnode) {return}// 获取对比 vnode 对应的真实的 DOM 节点const elm = vnode.elm = oldVnode.elm// 判断 oldVnode 和 vnode 是不是静态节点,如果是静态节点的话,直接 returnif (isTrue(vnode.isStatic) &&isTrue(oldVnode.isStatic) &&vnode.key === oldVnode.key &&(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {vnode.componentInstance = oldVnode.componentInstancereturn}// 获取新旧 vnode 的 childrenconst oldCh = oldVnode.childrenconst ch = vnode.childrenif (isUndef(vnode.text)) {//Vnode是元素节点或者text为空的文本节点if (isDef(oldCh) && isDef(ch)) {//Vnode和OldVnode都是元素节点,且都有子节点,更新子节点if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)} else if (isDef(ch)) {//Vnode和OldVnode都是元素节节点,但只有Vnode有子节点,添加子节点addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)} else if (isDef(oldCh)) {//Vnode和OldVnode都是元素节点,但只有OldVnode有子节点,删除子节点removeVnodes(elm, oldCh, 0, oldCh.length - 1)} else if (isDef(oldVnode.text)) {//Vnode和OldVnode都是文本节点,但只有oldVnode有text,删除textnodeOps.setTextContent(elm, '')}} else if (oldVnode.text !== vnode.text) {//Vnode有text属性,替换文本nodeOps.setTextContent(elm, vnode.text)}
}
updateChildren():我们继续分析,更新子节点函数updateChildren(),它是diff中最重要的环节.
直观上对子节点的更新方法是:循环 newChildren(新子节点列表),每循环到一个新的子节点,就去 oldChildren(旧的子节点列表)中循环查找与其相同的那个旧节点。如果在 oldChildren 中找不到的话,则说明当前循环的新子节点是新增节点,此时需要进行创建新节点并插入到视图的操作。如果找到了与当前循环节点相同的那个旧节点,此时需要进行更新节点的操作。另外还需要说明的一点是,如果找到了与当前循环节点相同的那个旧节点,但是位置不一样的话,除了做更新操作,还需要对节点的位置进行移动。当newChildren中的所有节点遍历完成,oldChildren中仍有剩余,则删除这些节点.
综上,对子节点的更新包含四个操作:
新增节点:插入未处理节点的前面
删除节点:newChildren遍历完成后,删除oldChildren中剩余的节点.
更新节点(同节点位置相同):与前文所述的更新节点操作一致
移动节点(同节点位置不同):更新,并把节点已知未处理节点最前面
Diff: 但这不是性能最优的解决方案,为此引入Diff算法来提高性能.他有两个特点:
1.比较只会在同层进行,不会跨层级比较
2.循环从两边向中间进行
1.第一步
对oldChildren和newChildren的开始和结束位置进行标记:oldStartIdx、oldEndIdx、newStartIdx、newEndIdx。
let oldStartIdx = 0 // 旧节点开始下标
let newStartIdx = 0 // 新节点开始下标
let oldEndIdx = oldCh.length - 1 // 旧节点结束下标
let oldStartVnode = oldCh[0] // 旧节点开始vnode
let oldEndVnode = oldCh[oldEndIdx] // 旧节点结束vnode
let newEndIdx = newCh.length - 1 // 新节点结束下标
let newStartVnode = newCh[0] // 新节点开始vnode
let newEndVnode = newCh[newEndIdx] // 新节点结束vnode
2.第二步
进入循环处理,分情况讨论并移动对应的VNode节点,退出条件是(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {....//处理逻辑
}
当newStartVnode == oldStartVnode时:
if (sameVnode(oldStartVnode, newStartVnode)) {patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)oldStartVnode = oldCh[++oldStartIdx]newStartVnode = newCh[++newStartIdx]}
当newEndVnode == oldEndVnode时:
else if (sameVnode(oldEndVnode, newEndVnode)) {patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)oldEndVnode = oldCh[--oldEndIdx]newEndVnode = newCh[--newEndIdx]}
以上两种情况直接调用patchVnode()完成更新节点逻辑,并且移动指针.
当oldStartVnode == newEndedVnode时:将节点移动到未处理节点最后面.
else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved rightpatchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))oldStartVnode = oldCh[++oldStartIdx]newEndVnode = newCh[--newEndIdx]}
当oldEndVnode == newStartVnode时:将节点移动到未处理节点最前面.
else if(sameVnode(oldEndVnode,newStartVnode))patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)oldEndVnode = oldCh[--oldEndIdx]newStartVnode = newCh[++newStartIdx]}
如此,已更新过的节点无论是内容还是位置都是正确的,更新完后就不需要在进行考虑了,我们只需要在未处理节点([start,end])之间进行移动和更新操作即可.
以上四种情况均不满足时:
else {// 没有找到相同的可以复用的节点,则新建节点处理/* 生成一个key与旧VNode的key对应的哈希表(只有第一次进来undefined的时候会生成,也为后面检测重复的key值做铺垫) 比如childre是这样的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}] beginIdx = 0 endIdx = 2 结果生成{key0: 0, key1: 1, key2: 2} */if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)/*如果newStartVnode新的VNode节点存在key并且这个key在oldVnode中能找到则返回这个节点的idxInOld(即第几个节点,下标)*/idxInOld = isDef(newStartVnode.key)? oldKeyToIdx[newStartVnode.key]: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)if (isUndef(idxInOld)) { // New element/*newStartVnode没有key或者是该key没有在老节点中找到则创建一个新的节点*/createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)} else {/*获取同key的老节点*/vnodeToMove = oldCh[idxInOld]if (sameVnode(vnodeToMove, newStartVnode)) {/*如果新VNode与得到的有相同key的节点是同一个VNode则进行patchVnode*/patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)//因为已经patchVnode进去了,所以将这个老节点赋值undefinedoldCh[idxInOld] = undefined/*当有标识位canMove实可以直接插入oldStartVnode对应的真实Dom节点前面*/canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)} else {// same key but different element. treat as new element/*当新的VNode与找到的同样key的VNode不是sameVNode的时候(比如说tag不一样或者是有不一样type的input标签),创建一个新的节点*/createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)}}newStartVnode = newCh[++newStartIdx]}
大部分情况下,前面四种方式就可以找到相同的节点,所以节省了很多次循环操作.
3.第三步
退出While循环时,如果newChildren有剩余,则将剩余的节点都加入真实的DOM,如果oldChildren有剩余,则把剩余的节点都删除.至此,整个diff过程完成.
if (oldStartIdx > oldEndIdx) {/*全部比较完成以后,发现oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多, 所以这时候多出来的新节点需要一个一个创建出来加入到真实Dom中*/refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elmaddVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) //创建 newStartIdx - newEndIdx 之间的所有节点} else if (newStartIdx > newEndIdx) {/*如果全部比较完成以后发现newStartIdx > newEndIdx,则说明新节点已经遍历完了,老节点多于新节点,这个时候需要将多余的老节点从真实Dom中移除*/removeVnodes(oldCh, oldStartIdx, oldEndIdx) //移除 oldStartIdx - oldEndIdx 之间的所有节点}
小结:
1.本文探讨了Vue何时执行更新DOM元素(updateComponent)的具体流程.
2.本文探讨了通过如何比较Vnode和oldVnode,以及修改DOM的三种情况的逻辑(patch函数)
3.本文探讨了更新节点的详细逻辑(patchVnode)
4.本文探讨了更新子节点的详细逻辑(diff算法)
本文章仅为本人学习总结,如果有不足还请各位指出!
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
