VUE源码解析笔记之patch 算法

433 阅读4分钟

patch⼯作原理

patch的核⼼diff算法:通过同层的树节点进⾏⽐较⽽⾮对树进⾏逐层搜索遍历的⽅式,所以 时间复杂度只有O(n),是⼀种相当⾼效的算法。

同层级只做三件事:增删改。具体规则是:new VNode不存在就删;old VNode不存在就增;都存在就 ⽐较类型,类型不同直接替换、类型相同执⾏更新;

下面来看看vue源码中patch方法的实现:

	function patch(oldVnode, vnode, hydrating, removeOnly, parentElm,
    refElm) {
    /*new vnode不存在则删*/
    if (isUndef(vnode)) {
        if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
        return
    }
    let isInitialPatch = false
    const insertedVnodeQueue = []
    if (isUndef(oldVnode)) {
        /*o ldVnode不存在则创建新节点 */
        isInitialPatch = true
        createElm(vnode, insertedVnodeQueue, parentElm, refElm)
    } else {
        /*oldVnode有nodeType,说明传递进来⼀个DOM元素*/
        const isRealElement = isDef(oldVnode.nodeType)
        if (!isRealElement && sameVnode(oldVnode, vnode)) {
            /*是组件且是同⼀个节点的时候打补丁*/
            patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
        } else {
            /*传递进来oldVnode是真实dom元素*/
            if (isRealElement) {
                 /*如果不是服务端渲染或者合并到真实DOM失败,则创建一个空的VNode节点替换它*/
                oldVnode = emptyNodeAt(oldVnode)
            }
            /*取代现有元素:*/
            const oldElm = oldVnode.elm
            const parentElm = nodeOps.parentNode(oldElm)
            //创建⼀个新的dom
            createElm(
                vnode,
                insertedVnodeQueue,
                oldElm._leaveCb ? null : parentElm,
                nodeOps.nextSibling(oldElm)
            )
            if (isDef(parentElm)) {
                /*移除⽼节点*/
                removeVnodes(parentElm, [oldVnode], 0, 0)
            } else if (isDef(oldVnode.tag)) {
                /*调⽤destroy钩⼦*/
                invokeDestroyHook(oldVnode)
            }
        }
    }
    /*调⽤insert钩⼦*/
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
}

在对oldVnode和vnode类型判断中有个sameVnode方法,这个方法决定了是否需要对oldVnode和vnode进行diff及patch的过程。

sameVnode会对传入的两个vnode进行基本属性的比较,只有当基本属性相同的情况下才认为这个两个vnode只是局部发生了更新,然后才会对这两个vnode进行diff,如果两个vnode的基本属性存在不一致的情况,那么就会直接跳过diff的过程,进而依据vnode新建一个真实的dom,同时删除老的dom节点。

function sameVnode (a, b) {
 return (
  a.key === b.key &&
  a.tag === b.tag &&
  a.isComment === b.isComment &&
  isDef(a.data) === isDef(b.data) &&
  sameInputType(a, b)
 )
}

patch vnode实现

两个VNode类型相同,就执⾏更新操作,包括三种类型操作:属性更新PROPS、⽂本更新TEXT、⼦节 点更新REORDER

patchVnode具体规则如下:

  • 如果新旧VNode都是静态的,同时它们的key相同(代表同⼀节点),并且新的VNode是clone或 者是标记了v-once,那么只需要替换elm以及componentInstance即可。
  • 新⽼节点均有children⼦节点,则对⼦节点进⾏diff操作,调⽤updateChildren,这个 updateChildren也是diff的核⼼。
  • 如果⽼节点没有⼦节点⽽新节点存在⼦节点,先清空⽼节点DOM的⽂本内容,然后为当前DOM节 点加⼊⼦节点。
  • 当新节点没有⼦节点⽽⽼节点有⼦节点的时候,则移除该DOM节点的所有⼦节点。
  • 当新⽼节点都⽆⼦节点的时候,只是⽂本的替换。
/*patch VNode节点*/
function patchVnode(oldVnode, vnode, insertedVnodeQueue,
    ownerArray, index, removeOnly) {
    /*两个VNode节点相同则直接返回*/
    if (oldVnode === vnode) {
        return
    }
    if (isDef(vnode.elm) && isDef(ownerArray)) {
        // clone reused vnode
        vnode = ownerArray[index] = cloneVNode(vnode)
    }
    const elm = vnode.elm = oldVnode.elm
    /*
    如果新旧VNode都是静态的,同时它们的key相同(代表同⼀节点),
    并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染⼀次),
    那么只需要替换elm以及componentInstance即可。
     */
    if (isTrue(vnode.isStatic) &&
        isTrue(oldVnode.isStatic) &&
        vnode.key === oldVnode.key &&
        (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
        vnode.elm = oldVnode.elm
        vnode.componentInstance = oldVnode.componentInstance
        return
    }
    /*如果存在data.hook.prepatch则要先执⾏*/
    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
        i(oldVnode, vnode)
    }
    const oldCh = oldVnode.children
    const ch = vnode.children
    /*执⾏属性、事件、样式等等更新操作*/
    if (isDef(data) && isPatchable(vnode)) {
        for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)

        if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    /*开始判断children的各种情况*/
    /*如果这个VNode节点没有text⽂本时*/
    if (isUndef(vnode.text)) {
        if (isDef(oldCh) && isDef(ch)) {
            /*新⽼节点均有children⼦节点,则对⼦节点进⾏diff操作,调⽤updateChildren*/
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue,
                removeOnly)
        } else if (isDef(ch)) {
            /*如果⽼节点没有⼦节点⽽新节点存在⼦节点,先清空elm的⽂本内容,然后为当前节点加⼊⼦
            节点*/
            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        } else if (isDef(oldCh)) {
            /*当新节点没有⼦节点⽽⽼节点有⼦节点的时候,则移除所有ele的⼦节点*/
            removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        } else if (isDef(oldVnode.text)) {
            /*当新⽼节点都⽆⼦节点的时候,只是⽂本的替换,因为这个逻辑中新节点text不存在,所以
            清除ele⽂本*/
            nodeOps.setTextContent(elm, '')
        }
    } else if (oldVnode.text !== vnode.text) {
        /*当新⽼节点text不⼀样时,直接替换这段⽂本*/
        nodeOps.setTextContent(elm, vnode.text)
    }
    /*调⽤postpatch钩⼦*/
    if (isDef(data)) {
        if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
}