文章内容

用具体的例子解说patchVNode的具体流程,通过例子逐步剖析出patchVNode代码所代表的含意

前置常识

由上一篇文章Vue2源码-整体流程浅析能够晓得,当两个VNode是同一个VNode时,会触发patchVNode()的执行

prepatch()触发

function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {    //...    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {        i(oldVnode, vnode)    }    //...}

从上面代码能够晓得,prepatch()次要是调用了updateChildComponent(),这个办法的作用是将newVnode相干数据更新到旧的oldVnode.componentOptions

// vue/src/core/vdom/create-component.jsprepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {    const options = vnode.componentOptions    const child = vnode.componentInstance = oldVnode.componentInstance    updateChildComponent(        child,        options.propsData, // updated props        options.listeners, // updated listeners        vnode, // new parent vnode        options.children // new children    )}
// vue/src/core/instance/lifecycle.jsexport function updateChildComponent(    vm: Component, // 旧的Component对象    propsData: ?Object, // 新的props数据    listeners: ?Object, // 新的listeners数据    parentVnode: MountedComponentVNode, // 新的VNode数据    renderChildren: ?Array<VNode> // 新的children数据) {    vm.$options._parentVnode = parentVnode    vm.$vnode = parentVnode // update vm's placeholder node without re-render    if (vm._vnode) { // update child tree's parent        vm._vnode.parent = parentVnode    }    vm.$options._renderChildren = renderChildren    vm.$attrs = parentVnode.data.attrs || emptyObject    vm.$listeners = listeners || emptyObject    if (propsData && vm.$options.props) {        toggleObserving(false)        const props = vm._props        const propKeys = vm.$options._propKeys || []        for (let i = 0; i < propKeys.length; i++) {            const key = propKeys[i]            const propOptions: any = vm.$options.props // wtf flow?            props[key] = validateProp(key, propOptions, propsData, vm)        }        toggleObserving(true)        vm.$options.propsData = propsData    }    //...省略代码,根本跟下面代码逻辑统一,进行vm的更新而已}

触发update()办法执行

function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {    //...    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {        i(oldVnode, vnode)    }    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)    }    //...}
参考Vue2自定义指令的钩子函数:https://v2.cn.vuejs.org/v2/guide/custom-directive.html#ad

从文档能够晓得,update()的钩子函数,代表所在组件的 VNode 更新时调用,然而可能产生在其子 VNode 更新之前。指令的值可能产生了扭转,也可能没有。然而你能够通过比拟更新前后的值来疏忽不必要的模板更新
比方上面的示例代码,如果咱们注册了一个test33的自定义指令,那么<div v-test33></div>所在组件的 VNode 更新时调用注册的update()办法

// html<template>    <div v-test33></div></template>// jsVue.directive('test33', {    update: function (el) {        console.error("测试update 自定义指令");    }})

分状况进行children的比拟

function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {    //...    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {        i(oldVnode, vnode)    }    const oldCh = oldVnode.children    const ch = vnode.children    //...    if (isUndef(vnode.text)) {        if (isDef(oldCh) && isDef(ch)) {            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)        } else if (isDef(ch)) {            if (process.env.NODE_ENV !== 'production') {                checkDuplicateKeys(ch)            }            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)        } else if (isDef(oldCh)) {            removeVnodes(oldCh, 0, oldCh.length - 1)        } else if (isDef(oldVnode.text)) {            nodeOps.setTextContent(elm, '')        }    } else if (oldVnode.text !== vnode.text) {        nodeOps.setTextContent(elm, vnode.text)    }}

从下面的代码能够晓得,当prepatch()newVNode的相干数据更新到oldVNode.componentOptions后,会进行它们children的比拟,确定它们children的更新策略,总结下来为

  • oldVNodeChildren!==newVNodeChildren,触发updateChildren()办法
  • oldVNodeChildren为空,newVNodeChildren不为空,执行newVNodeChildren新建插入操作
  • oldVNodeChildren不为空,newVNodeChildren为空,执行oldVNodeChildren删除操作
  • 如果是文本节点,则更新文本内容

外围局部updateChildren()

前置阐明

  • 因为流程过于简单,将应用一个具体的例子阐明源码的流程
  • patchVnode(oldVnode, newVnode, insertedVnodeQueue, newCh, newIdx):当oldVnode=newVnode的时候,才会触发patchVnode()办法,进行新旧VNodeData的更新,而后进行children元素的比拟,进行新增删除/向下比拟触发updateChildren(),除了更新数据(同个VNode进行数据更新)、解决children(addVNode/removeVNode/updateVNode-选择性更新),不做其它解决

    上面所有图中旧的children真实情况下应该存在oldVNode的DOM援用状况,为了偷懒,会将矩形外边的节点当作oldVNode援用DOM的地位,矩形包裹才是旧VNode的地位,因而会呈现状况5中就算挪动了元素到其它地位,依然存在元素在oldVNode中的状况

条件分类代码

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {    if (sameVnode(oldStartVnode, newStartVnode)) {        // 状况1: oldStartIdx === newStartIdx    } else if (sameVnode(oldEndVnode, newEndVnode)) {        // 状况2: oldEndIdx === newEndIdx    } else if (sameVnode(oldStartVnode, newEndVnode)) {         // 状况3: oldStartIdx === newEndIdx    } else if (sameVnode(oldEndVnode, newStartVnode)) {         // 状况4: oldEndIdx === newStartIdx    } else {        // 状况5: 四端都找不到同样的VNode    }}

状况4: oldEndIdx===newStartIdx

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {    if (sameVnode(oldStartVnode, newStartVnode)) {        // 状况1: oldStartIdx === newStartIdx    } else if (sameVnode(oldEndVnode, newEndVnode)) {        // 状况2: oldEndIdx === newEndIdx    } else if (sameVnode(oldStartVnode, newEndVnode)) {        // 状况3: oldStartIdx === newEndIdx    } else if (sameVnode(oldEndVnode, newStartVnode)) {        // 状况4: oldEndIdx === newStartIdx        // 数据更新,如果有children,也在patchVnode()外部进行调用解决,addVNode/removeVNode/updateChildren        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)        // 调整目前DOM元素的地位,将oldEndVnode的DOM挪动到oldStartVnode的DOM后面        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)        oldEndVnode = oldCh[--oldEndIdx] // 更新索引        newStartVnode = newCh[++newStartIdx] // 更新索引    } else {        // 状况5: 四端都找不到同样的VNode    }}

状况2: oldEndIdx===newEndIdx

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {    if (sameVnode(oldStartVnode, newStartVnode)) {        // 状况1: oldStartIdx === newStartIdx    } else if (sameVnode(oldEndVnode, newEndVnode)) {        // 状况2: oldEndIdx === newEndIdx        // 数据更新,如果有children,也在patchVnode()外部进行调用解决,addVNode/removeVNode/updateChildren        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)        oldEndVnode = oldCh[--oldEndIdx]        newEndVnode = newCh[--newEndIdx]    } else if (sameVnode(oldStartVnode, newEndVnode)) {        // 状况3: oldStartIdx === newEndIdx    } else if (sameVnode(oldEndVnode, newStartVnode)) {        // 状况4: oldEndIdx === newStartIdx        //......    } else {        // 状况5: 四端都找不到同样的VNode    }}

状况3: oldStartIdx === newEndIdx

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {    if (sameVnode(oldStartVnode, newStartVnode)) {        // 状况1: oldStartIdx === newStartIdx    } else if (sameVnode(oldEndVnode, newEndVnode)) {        // 状况2: oldEndIdx === newEndIdx        //....    } else if (sameVnode(oldStartVnode, newEndVnode)) {        // 状况3: oldStartIdx === newEndIdx        // 数据更新,如果有children,也在patchVnode()外部进行调用解决,addVNode/removeVNode/updateChildren        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)        // 调整目前DOM元素的地位,将oldStartVnode的DOM挪动到oldEndVnode的DOM前面        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))        oldStartVnode = oldCh[++oldStartIdx]        newEndVnode = newCh[--newEndIdx]    } else if (sameVnode(oldEndVnode, newStartVnode)) {        // 状况4: oldEndIdx === newStartIdx        //......    } else {        // 状况5: 四端都找不到同样的VNode    }}

状况1: oldStartIdx === newStartIdx

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {    if (sameVnode(oldStartVnode, newStartVnode)) {        // 状况1: oldStartIdx === newStartIdx        // 数据更新,如果有children,也在patchVnode()外部进行调用解决,addVNode/removeVNode/updateChildren        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)        oldStartVnode = oldCh[++oldStartIdx]        newStartVnode = newCh[++newStartIdx]    } else if (sameVnode(oldEndVnode, newEndVnode)) {        // 状况2: oldEndIdx === newEndIdx        //....    } else if (sameVnode(oldStartVnode, newEndVnode)) {        // 状况3: oldStartIdx === newEndIdx        //....    } else if (sameVnode(oldEndVnode, newStartVnode)) {        // 状况4: oldEndIdx === newStartIdx        //......    } else {        // 状况5: 四端都找不到同样的VNode    }}

状况5: 四端都无奈找到能够复用,雷同的VNode

四端无奈找到可匹配的VNode,然而两头能够找到

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {    if (sameVnode(oldStartVnode, newStartVnode)) {        // 状况1: oldStartIdx === newStartIdx    } else if (sameVnode(oldEndVnode, newEndVnode)) {        // 状况2: oldEndIdx === newEndIdx    } else if (sameVnode(oldStartVnode, newEndVnode)) {        // 状况3: oldStartIdx === newEndIdx    } else if (sameVnode(oldEndVnode, newStartVnode)) {        // 状况4: oldEndIdx === newStartIdx    } else {        // 状况5: 四端都找不到同样的VNode        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            // ...         } else {            // 命中上面代码            vnodeToMove = oldCh[idxInOld]            if (sameVnode(vnodeToMove, newStartVnode)) {                // 数据更新,如果有children,也在patchVnode()外部进行调用解决,addVNode/removeVNode/updateChildren                patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)                // 将索引中的oldCh[idxInOld]置为空,不便前面间接跳过                oldCh[idxInOld] = undefined                // 调整目前DOM元素的地位,将oldCh[idxInOld]的DOM挪动到oldStartVnode后面                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]    }}
(新条件1)oldStartVnode=undefine:两头区域可复用区域间接跳过下一次循环

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {    if (isUndef(oldStartVnode)) {        // 新条件1:oldStartVnode=undefine        oldStartVnode = oldCh[++oldStartIdx] // 状况5置为undefined的startVNode    } else if (sameVnode(oldStartVnode, newStartVnode)) {        // 状况1: oldStartIdx === newStartIdx    } else if (sameVnode(oldEndVnode, newEndVnode)) {        // 状况2: oldEndIdx === newEndIdx    } else if (sameVnode(oldStartVnode, newEndVnode)) {        // 状况3: oldStartIdx === newEndIdx    } else if (sameVnode(oldEndVnode, newStartVnode)) {        // 状况4: oldEndIdx === newStartIdx    } else {        // 状况5: 四端都找不到同样的VNode        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            // ...         } else {            // 命中上面代码            vnodeToMove = oldCh[idxInOld]            if (sameVnode(vnodeToMove, newStartVnode)) {                // ...                oldCh[idxInOld] = undefined                // ...            } else {                // ...            }        }        newStartVnode = newCh[++newStartIdx]    }}
四端无奈找到可匹配的VNode,两头也无奈找到,属于新增元素

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {    if (isUndef(oldStartVnode)) {        oldStartVnode = oldCh[++oldStartIdx] // 状况5置为undefined的startVNode    } else if (sameVnode(oldStartVnode, newStartVnode)) {        // 状况1: oldStartIdx === newStartIdx    } else if (sameVnode(oldEndVnode, newEndVnode)) {        // 状况2: oldEndIdx === newEndIdx    } else if (sameVnode(oldStartVnode, newEndVnode)) {        // 状况3: oldStartIdx === newEndIdx    } else if (sameVnode(oldEndVnode, newStartVnode)) {        // 状况4: oldEndIdx === newStartIdx    } else {        // 状况5: 四端都找不到同样的VNode        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)        idxInOld = isDef(newStartVnode.key)            ? oldKeyToIdx[newStartVnode.key]            : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)        if (isUndef(idxInOld)) {             // 命中上面代码,新增元素并且插入oldStartVNode对应的DOM后面            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)        } else {            vnodeToMove = oldCh[idxInOld]            if (sameVnode(vnodeToMove, newStartVnode)) {                // ...                oldCh[idxInOld] = undefined                // ...            } else {                // ...            }        }        newStartVnode = newCh[++newStartIdx]    }}

(循环完结1)newStartIdx > newEndIdx

完结第一个循环须要删除不必的旧VNode

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {    if (isUndef(oldStartVnode)) {        // 新条件1:oldStartVnode=undefine        oldStartVnode = oldCh[++oldStartIdx] // 状况5置为undefined的startVNode    } else if (sameVnode(oldStartVnode, newStartVnode)) {        // 状况1: oldStartIdx === newStartIdx    } else if (sameVnode(oldEndVnode, newEndVnode)) {        // 状况2: oldEndIdx === newEndIdx    } else if (sameVnode(oldStartVnode, newEndVnode)) {        // 状况3: oldStartIdx === newEndIdx    } else if (sameVnode(oldEndVnode, newStartVnode)) {        // 状况4: oldEndIdx === newStartIdx    } else {        // 状况5: 四端都找不到同样的VNode,两头可复用,挪动DOM/两头不可复用,新增插入DOM        if (isUndef(idxInOld)) {            // 四端都找不到同样的VNode,两头不可复用,新增插入DOM        } else {            vnodeToMove = oldCh[idxInOld]            if (sameVnode(vnodeToMove, newStartVnode)) {                // 四端都找不到同样的VNode,两头可复用,挪动DOM            } else {                // key雷同,其它条件不同当作新VNode解决,即两头不可复用,新增插入DOM解决            }        }    }}if (newStartIdx > newEndIdx) {    // 循环完结1:删除曾经废除的旧VNode    removeVnodes(oldCh, oldStartIdx, oldEndIdx)}
从下面剖析代码能够看出,条件中还短少oldEndVnode不存在以及完结第一个循环时,oldStartIdx > oldEndIdx两种可能性的判断,事实上这两种状况也有可能产生

(新条件2)oldEndVnode=undefine

临时还没想好场景,先作为补齐条件判断退出
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {    if (isUndef(oldStartVnode)) {        // 新条件1:oldStartVnode=undefine        oldStartVnode = oldCh[++oldStartIdx] // 状况5置为undefined的startVNode    } else if (isUndef(oldEndVnode)) {        // 新条件2:oldEndVnode=undefine        oldEndVnode = oldCh[--oldEndIdx]    } else if (sameVnode(oldStartVnode, newStartVnode)) {        // 状况1: oldStartIdx === newStartIdx    } else if (sameVnode(oldEndVnode, newEndVnode)) {        // 状况2: oldEndIdx === newEndIdx    } else if (sameVnode(oldStartVnode, newEndVnode)) {        // 状况3: oldStartIdx === newEndIdx    } else if (sameVnode(oldEndVnode, newStartVnode)) {        // 状况4: oldEndIdx === newStartIdx    } else {        // 状况5: 四端都找不到同样的VNode,两头可复用,挪动DOM/两头不可复用,新增插入DOM        if (isUndef(idxInOld)) {            // 四端都找不到同样的VNode,两头不可复用,新增插入DOM        } else {            vnodeToMove = oldCh[idxInOld]            if (sameVnode(vnodeToMove, newStartVnode)) {                // 四端都找不到同样的VNode,两头可复用,挪动DOM            } else {                // key雷同,其它条件不同当作新VNode解决,即两头不可复用,新增插入DOM解决            }        }    }}if (newStartIdx > newEndIdx) {    // 删除曾经废除的旧VNode    removeVnodes(oldCh, oldStartIdx, oldEndIdx)}

(循环完结2)oldStartIdx>oldEndIdx

由下面的剖析能够晓得,一开始会触发状况1: oldStartIdx === newStartIdx,而后oldStartIdx++oldEndIdx++,此时因为oldStartIdx>oldEndIdx,循环完结

从上图能够看出,目前newCh还有一个VNode未解决,因而须要遍历[newStartIdx, newEndIdx]进行残余元素的增加解决,如上面代码所示,循环区间,进行createElm()操作

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {    if (isUndef(oldStartVnode)) {        // 新条件1:oldStartVnode=undefine        oldStartVnode = oldCh[++oldStartIdx] // 状况5置为undefined的startVNode    } else if (isUndef(oldEndVnode)) {        // 新条件2:oldEndVnode=undefine        oldEndVnode = oldCh[--oldEndIdx]    } else if (sameVnode(oldStartVnode, newStartVnode)) {        // 状况1: oldStartIdx === newStartIdx    } else if (sameVnode(oldEndVnode, newEndVnode)) {        // 状况2: oldEndIdx === newEndIdx    } else if (sameVnode(oldStartVnode, newEndVnode)) {        // 状况3: oldStartIdx === newEndIdx    } else if (sameVnode(oldEndVnode, newStartVnode)) {        // 状况4: oldEndIdx === newStartIdx    } else {        // 状况5: 四端都找不到同样的VNode,两头可复用,挪动DOM/两头不可复用,新增插入DOM        if (isUndef(idxInOld)) {            // 四端都找不到同样的VNode,两头不可复用,新增插入DOM        } else {            vnodeToMove = oldCh[idxInOld]            if (sameVnode(vnodeToMove, newStartVnode)) {                // 四端都找不到同样的VNode,两头可复用,挪动DOM            } else {                // key雷同,其它条件不同当作新VNode解决,即两头不可复用,新增插入DOM解决            }        }    }}if (oldStartIdx > oldEndIdx) {    // 如果新的children最初一个节点曾经解决实现,那么初始化refElm为最初一个节点    // 由上面insert办法能够晓得,插入元素会nodeOps.insertBefore(parent, elm, ref)    // 一直在ref后面插入[newStartIdx,newEndIdx]的DOM    // 因为ref统一都不会动,因而[newStartIdx,newEndIdx]能够程序插入    // 如果新的children最初一个节点还没解决,那么解决refElm=null    // 由上面insert办法能够晓得,插入元素会nodeOps.appendChild(parent, elm)    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm    // addVnodes:[newStartIdx,newEndIdx]遍历调用createElm插入DOM元素    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)} else if (newStartIdx > newEndIdx) {    // 循环完结1: 删除多余的oldVNode}// addVnodes->createElm()->insert()function insert(parent, elm, ref) {    if (isDef(parent)) {        if (isDef(ref)) {            if (nodeOps.parentNode(ref) === parent) {                nodeOps.insertBefore(parent, elm, ref)            }        } else {            nodeOps.appendChild(parent, elm)        }    }}

触发postpatch()执行

if (isDef(data)) {    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)}
参考Vue2自定义指令的钩子函数:https://v2.cn.vuejs.org/v2/guide/custom-directive.html#ad

从上面的源码剖析和文档能够晓得,postpatch()实际上调用的是componentUpdated()的钩子函数,代表指令所在组件的 VNode 及其子 VNode 全副更新后调用

if (dirsWithPostpatch.length) {    mergeVNodeHook(vnode, 'postpatch', function () {        for (var i = 0; i < dirsWithPostpatch.length; i++) {            callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);        }    });}

比方上面的示例代码,如果咱们注册了一个test33的自定义指令,那么<div v-test33></div>所在组件的 VNode 及其子 VNode 全副更新后就会调用注册的componentUpdated()办法

// html<template>    <div v-test33></div></template>//jsVue.directive('test33', {    componentUpdated: function () {        console.error("测试componentUpdated");    }})

参考文章

  1. vue-design