Vue 源码patch过程详解

5,903 阅读13分钟

在这篇文章深入源码学习Vue响应式原理讲解了当数据更改时,Vue是如何通知订阅者进行更新的,这篇文章讲得就是:视图知道了依赖的数据的更改,如何将新的数据反映在视图上。

Vnode Tree

在真实的HTML中有DOM树与之对应,在Vue中也有类似的Vnode Tree与之对应。

抽象DOM

jquery时代,实现一个功能,往往是直接对DOM进行操作来达到改变视图的目的。但是我们知道直接操作DOM往往会影响重绘和重排,这两个是最影响性能的两个元素。
进入Virtual DOM时代以后,将真实的DOM树抽象成了由js对象构成的抽象树。virtual DOM就是对真实DOM的抽象,用属性来描述真实DOM的各种特性。当virtual DOM发生改变时,就去修改视图。在Vue中就是Vnode Tree的概念

VNode

当修改某条数据的时候,这时候js会将整个DOM Tree进行替换,这种操作是相当消耗性能的。所以在Vue中引入了Vnode的概念:Vnode是对真实DOM节点的模拟,可以对Vnode Tree进行增加节点、删除节点和修改节点操作。这些过程都只需要操作VNode Tree,不需要操作真实的DOM,大大的提升了性能。修改之后使用diff算法计算出修改的最小单位,在将这些小单位的视图进行更新。

// core/vdom/vnode.js
class Vnode {
    constructor(tag, data, children, text, elm, context, componentOptions) {
        // ...
    }
}

生成vnode

生成vnode有两种情况:

  1. 创建非组件节点的vnode
    • tag不存在,创建空节点、注释、文本节点
    • 使用vue内部列出的元素类型的vnode
    • 没有列出的创建元素类型的vnode

<p>123</p>为例,会被生成两个vnode:

  • tagp,但是没有text值的节点
  • 另一个是没有tag类型,但是有text值的节点
  1. 创建组件节点的VNode 组件节点生成的Vnode,不会和DOM Tree的节点一一对应,只存在VNode Tree
    // core/vdom/create-component
    function createComponent() {
        // ...
        const vnode = new VNode(
            `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
            data, undefined, undefined, undefined, context,
            { Ctor, propsData, listeners, tag, children }
        )
    }
    
    这里创建一个组件占位vnode,也就不会有真实的DOM节点与之对应

组件vnode的建立,结合下面例子进行讲解:

<!--parent.vue-->
<div classs="parent">
    <child></child>
</div>
<!--child.vue-->
<template>
    <div class="child"></div>
</template>

真实渲染出来的DOM Tree是不会存在child这个标签的。child.vue是一个子组件,在Vue中会给这个组件创建一个占位的vnode,这个vnode在最终的DOM Tree不会与DOM节点一一对应,即只会出现vnode Tree中。

/* core/vdom/create-component.js */
export function createComponent () {
    // ...
     const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children }
    )
}

那最后生成的Vnode Tree就大概如下:

vue-component-${cid}-parent
    vue-component-${cid}-child
        div.child

最后生成的DOM结构为:

<div class="parent">
    <div class="child"></div>
</div>

在两个组件文件中打印自身,可以看出两者之间的关系 chlid实例对象

parent实例对象
可以看到以下关系:

  1. vnode通过children指向子vnode
  2. vnode通过$parent指向父vnode
  3. 占位vnode为实例的$vnode
  4. 渲染的vnode为对象的_vnode

patch

在上一篇文章提到当创建Vue实例的时候,会执行以下代码:

updateComponent = () => {
    const vnode = vm._render();
    vm._update(vnode)
}
vm._watcher = new Watcher(vm, updateComponent, noop)

例如当data中定义了一个变量a,并且模板中也使用了它,那么这里生成的Watcher就会加入到a的订阅者列表中。当a发生改变时,对应的订阅者收到变动信息,这时候就会触发Watcherupdate方法,实际update最后调用的就是在这里声明的updateComponent
当数据发生改变时会触发回调函数updateComponentupdateComponent是对patch过程的封装。patch的本质是将新旧vnode进行比较,创建、删除或者更新DOM节点/组件实例。

// core/vdom/patch.js
function createPatchFunction(backend) {
    const { modules, nodeOps } = backend;
    for (i = 0; i < hooks.length; ++i) {
        cbs[hooks[i]] = []
        for (j = 0; j < modules.length; ++j) {
          if (isDef(modules[j][hooks[i]])) {
            cbs[hooks[i]].push(modules[j][hooks[i]])
          }
        }
    }
    
    return function patch(oldVnode, vnode) {
        if (isUndef(oldVnode)) {
            let isInitialPatch = true
            createElm(vnode, insertedVnodeQueue, parentElm, refElm)
        } else {
            const isRealElement = isDef(oldVnode.nodeType)
            if (!isRealElement && sameVnode(oldVnode, vnode)) {
                patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
            } else {
                if (isRealElement) {
                    oldVnode = emptyNodeAt(oldVnode)
                }
                const oldElm = oldVnode.elm
                const parentElm = nodeOps.parentNode(oldElm)
                createElm(
                    vnode,
                    insertedVnodeQueue,
                    oldElm._leaveC ? null : parentELm,
                    nodeOps.nextSibling(oldElm)
                )
                
                if (isDef(vnode.parent)) {
                    let ancestor = vnode.parent;
                    while(ancestor) {
                        ancestor.elm = vnode.elm;
                        ancestor = ancestor.parent
                    }
                    if (isPatchable(vnode)) {
                        for (let i = 0; i < cbs.create.length; ++i) {
                            cbs.create[i](emptyNode, vnode.parent)
                        }
                    }
                }
                if (isDef(parentElm)) {
                    removeVnodes(parentElm, [oldVnode], 0, 0)
                } else if (isDef(oldVnode.tag)) {
                    invokeDestroyHook(oldVnode)
                }
            }
        }
        
        invokeInsertHook(vnode, insertedVnodeQueue)
        return vode.elm
    }
}
  • 如果是首次patch,就创建一个新的节点
  • 老节点存在
    • 老节点不是真实DOM并且和新节点相似
      • 调用patchVnode修改现有节点
    • 新老节点不相同
      • 如果老节点是真实DOM,创建对应的vnode节点
      • 为新的Vnode创建元素/组件实例,若parentElm存在,则插入到父元素上
      • 如果组件根节点被替换,遍历更新父节点elm
      • 然后移除老节点
  • 调用insert钩子
    • 是首次patch并且vnode.parent存在,设置vnode.parent.data.pendingInsert = queue
    • 如果不满足上面条件则对每个vnode调用insert钩子
  • 返回vnode.elm真实DOM内容 nodeOps上封装了针对各种平台对于DOM的操作,modules表示各种模块,这些模块都提供了createupdate钩子,用于创建完成和更新完成后处理对应的模块;有些模块还提供了activateremovedestory等钩子。经过处理后cbs的最终结构为:
cbs = {
    create: [
        attrs.create,
        events.create
        // ...
    ]
}

可以看到的是只有当oldVnodevnode满足sameVnode的时候,并且新vnode都是vnode节点,不是真实的DOM节点。 其他情况要么创建,要么进行删除。
当下面情况时出现时就会出现根节点被替换的情况:

<!-- parent.vue -->
<template>
    <child></child>
</template>
<!-- child.vue -->
<template>
    <div class="child">
        child
    </div>
</template>

这个时候parent生成的vnode.elm就是div.child的内容。 patch函数最后返回了经过一系列处理的vnode.elm也就是真实的DOM内容。

createElm

createElm的目的创建VNode节点的vnode.elm。不同类型的VNode,其vnode.elm创建过程也不一样。对于组件占位VNode,会调用createComponent来创建组件占位VNode的组件实例;对于非组件占位VNode会创建对应的DOM节点。 现在有三种节点:

  • 元素类型的VNode:
    • 创建vnode对应的DOM元素节点vnode.elm
    • 设置vnodescope
    • 调用createChildren遍历子节点创建对应的DOM节点
    • 执行create钩子函数
    • DOM元素插入到父元素中
  • 注释和本文节点
    • 创建注释/文本节点vnode.elm,并插入到父元素中
  • 组件节点:调用createComponent
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested) {
    // 创建一个组件节点
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
        return
    }
    const data = vnode.data;
    const childre = vnode.children;
    const tag = vnode.tag;
    // ...

    if (isDef(tag)) {
        vnode.elm = vnode.ns
            ? nodeOps.createElementNS(vnode.ns, tag)
            : nodeOps.createElement(tag, vnode)
        setScope(vnode)
        if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        createChildren(vnode, children, insertedVnodeQueue)  
    } else if (isTrue(vnode.isComment)) {
        vnode.elm = nodeOps.createComment(vnode.text);
    } else {
        vnode.elm = nodeOps.createTextNode(vnode.te)
    }
    insert(parentElm, vnode.elm, refElm)
}

createComponent的主要作用是在于创建组件占位Vnode的组件实例, 初始化组件,并且重新激活组件。在重新激活组件中使用insert方法操作DOMcreateChildren用于创建子节点,如果子节点是数组,则遍历执行createElm方法,如果子节点的text属性有数据,则使用nodeOps.appendChild()在真实DOM中插入文本内容。insert用将元素插入到真实DOM中。

// core/vdom/patch.js
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    // ...
    let i = vnode.data.hook.init
    i(vnode, false, parentElm, refElm)
    if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm)
        return true;
    }
}
function initComponent(vnode, insertedVnodeQueue) {
    /* 把之前的已经存在的Vnode队列合并进去 */
    if (isDef(vnode.data.pendingInsert)) {
        insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
    }
    vnode.elm = vnode.componentInstance.$el;
    if (isPatchable(vnode)) {
        // 调用create钩子
        invokeCreateHooks(vnode, insertedVnodeQueue);
        // 为scoped css设置scoped id
        setScope(vnode)
    } else {
        // 注册ref
        registerRef(vnode);
        insertedVnodeQueue.push(vnode)
    }
}
  • 执行init钩子生成componentInstance组件实例
  • 调用initComponent初始化组件
    • 把之前已经存在的vnode队列进行合并
    • 获取到组件实例的DOM根元素节点,赋给vnode.elm
    • 如果vnode是可patch
      • 调用create函数,设置scope
    • 如果不可patch
      • 注册组件的ref,把组件占位vnode加入insertedVnodeQueue
  • vnode.elm插入到DOM Tree中 在createComponent中,首先获取 在组件创建过程中会调用core/vdom/create-component中的createComponent,这个函数会创建一个组件VNode,然后会再vnode上创建声明各个声明周期函数,init就是其中的一个周期,他会为vnode创建componentInstance属性,这里componentInstance表示继承Vue的一个实例。在进行new vnodeComponentOptions.Ctor(options)的时候就会重新创建一个vue实例,也就会重新把各个生命周期执行一遍如created-->mounted
init (vnode) {
    // 创建子组件实例
    const child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance)
    chid.$mount(undefined)
}
function createComponentInstanceForVnode(vn) {
    // ... options的定义
    return new vnodeComponentOptions.Ctor(options)
}

这样child就表示一个Vue实例,在实例创建的过程中,会执行各种初始化操作, 例如调用各个生命周期。然后调用$mount,实际上会调用mountComponent函数。

// core/instance/lifecycle
function mountComponent(vm, el) {
    // ...
    updateComponent = () => {
        vm._update(vm._render())
    }
    vm._watcher = new Watcher(vm, updateComponent, noop)
}

在这里就会执行vm._render

// core/instance/render.js
Vue.propotype._render = function () {
    // ...
    vnode = render.call(vm._renderProxy, vm.$createElement)
    return vnode
}

可以看到的时候调用_render函数,最后生成了一个vnode。然后调用vm._update进而调用vm.__patch__生成组件的DOM Tree,但是不会把DOM Tree插入到父元素上,如果子组件中还有子组件,就会创建子孙组件的实例,创建子孙组件的DOM Tree。当调用insert(parentElm, vnode.elm, refElm)才会将当前的DOM Tree插入到父元素中。
在回到patch函数,当不是第一次渲染的时候,就会执行到另外的逻辑,然后oldVnode是否为真实的DOM,如果不是,并且新老VNode不相同,就执行patchVnode

// core/vdom/patch.js
function sameVnode(a, b) {
    return (
        a.key === b.key &&
        a.tag === b.tag && 
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType
    )
}

sameVnode就是用于判断两个vnode是否是同一个节点。

insertedVnodeQueue的作用

在当前patch过程中,有一个数组insertedVnodeQueue,这是干嘛的,从单词上来看就是对这个队列中的vnode调用inserted钩子。在patch函数中最后调用了invokeInserthook

function invokeInsertHook(vnode, queue, initial) {
    if (isTrue(initial) && isDef(vnode.parent)) {
        vnode.parent.data.pendingInsert = queue;
    } else {
        for (let i = 0; i < queue.length; ++i) {
            queue[i].data.hook.insert(queue[i])
        }
    }
}

当不是首次patch并且vnode.parent不存在的时候,就会对insertedVnodeQueuevnode进行遍历,依次调用inserted钩子。 那什么时候对insertedVnodeQueue进行修改的呢。

function createElm() {
    // ...
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
        return
    }
    if (isDef(tag)) {
        if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue);
        }
    }
}
function initComponent(vnode, insertedVnodeQueue) {
    if (isDef(vnode.data.pendingInsert)) {
        insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
    }
    if (isPatchable) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
    } else {
        insertedVnodeQueue.push(vnode)
    }
}
function invokeCreateHooks(vnode, insertedVnodeQueue) {
    // ...
    insertedVnodeQueue.push(vnode);
}

在源码中可以看到在createElm中对组件节点和非组件节点都对insertedVnodeQueue进行了操作,每创建一个组件节点或非组件节点的时候就会往insertedVnodeQueuepush当前的vnode,最后对insertedVnodeQueue中所有的vnode调用inserted钩子。 但是当子组件首次渲染完成以后,invokeInsertHook中不会立即调用insertedVnodeQueue中各个Vnodeinsert方法,而是将insertedVnodeQueue转存至父组件占位vnodevnode.data.pendingInert上,当父组件执行initComponent的时候,将子组件传递过来的insertedVnodeQueue和自身的insertedVnodeQueue进行连接,最后调用父组件的insertedVnodeQueue中各个vnodeinsert方法。

Vnode的生命周期

createPatchFunction中会传入参数backend

function createPatchFunction (backend) {
    const { modules, nodeOps } = backend;
}

nodeOps是各种平台对DOM节点操作的适配,例如web或者weex modules是各种平台的模块,以web为例: Web平台相关模块: - attrs模块: 处理节点上的特性attribute - klass模块:处理节点上的类class - events模块: 处理节点上的原生事件 - domProps模块: 处理节点上的属性property - style模块: 处理节点上的内联样式style特性 - trasition模块 核心模块: - ref模块:处理节点上的引用ref - directives模块: 处理节点上的指令directives 每个功能模块都包含了各种钩子,用于DOM节点创建、更新和销毁。
Vnode中存在各种生命周期如: - create:DOM元素节点创建时/初始化组件时调用 - activate: 组件激活时调用 - update: DOM节点更新时调用 - remove: DOM节点移除时调用 - destory: 组件销毁时调用 那这些生命周期是如何加入的,回到最开始的地方:

vnode = vm.render();
Vue.prototype._render = function () {
    const vm = this;
    const {
        render,
    } = vm.$options;
    vnode = render.call(vm._renderProxy, vm.$createElement)
    return vnode;
}

vnode是由render.call(vm._renderProxy, vm.$createElement)生成的。
这里的render有两种情况:

  1. 基于HTML的模板形式,即template选项
  2. 用于手写的render函数形式 使用template形式的模板最终转换为render函数的形式。vm.$createElement返回的就是vnodecreateElementvdom/create-element中,对于真实的DOM还是组件类型用不同的方式创建相应的vnode
  3. 真实节点调用vnode = new VNode(tag, data, children, undefined, undefined, context)
  4. 组件节点调用createComponent(Ctor, data, context, children, tag) createComponent定义在vdom/create-component
function createComponent(Ctor, data, context, children, tag) {
    mergeHooks();
}
const componentVnodeHooks = {
    init(){},
    prepatch(){},
    insert(){},
    destory(){}
}
function mergeHooks(data) {
    if (!data.hook) {
        data.hook = {}
    }
    const hooksToMerge = Object.keys(componentVNodeHooks)
    for (let i = 0; i < hooksToMerge.length; i++) {
        const key = hooksToMerge[i];
        const fromParent = data.hook[key]
        const ours = componentVNodeHooks[key];
        data.hook[key] = fromParent ? mergeHook(ours, fromParent) : ours;
    }
}

在这里就给vnode.data.hook上绑定了各种钩子initprepatchinsertdestroy。在patch过程中,就会调用对应的钩子。

patchVnode

如果符合sameVnode,就不会渲染vnode重新创建DOM节点,而是在原有的DOM节点上进行修补,尽可能复用原有的DOM节点。

  • 如果两个节点相同则直接返回
  • 处理静态节点的情况
  • vnode是可patch
    • 调用组件占位vnodeprepatch钩子
    • update钩子存在,调用update钩子
  • vnode不存在text文本
    • 新老节点都有children子节点,且children不相同,则调用updateChildren递归更新children(这个函数的内容放到diff中进行讲解)
    • 只有新节点有子节点:先清空文本内容,然后为当前节点添加子节点
    • 只有老节点存在子节点: 移除所有子节点
    • 都没有子节点的时候,就直接移除节点的文本
  • 新老节点文本不一样: 替换节点文本
  • 调用vnodepostpatch钩子
function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    if (oldVnode === vnode) return
    // 静态节点的处理程序
    const data = vnode.data;
    i = data.hook.prepatch
    i(oldVnode, vnode);
    if (isPatchable(vnode)) {
        for(i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
        i = data.hook.update
        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 (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
        nodeOps.setTextContent(elm, vnode.text)
    }
    i = data.hook.postpatch
    i(oldVnode, vnode)
}

diff算法

patchVnode中提到,如果新老节点都有子节点,但是不相同的时候就会调用updateChildren,这个函数通过diff算法尽可能的复用先前的DOM节点。

function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
    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, elmToMove, refElm 
    
    while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (isUndef(oldStartVnode)) {
            oldStartVnode = oldCh[++oldStartIdx]
        } else if (isUndef(oldEndVnode)) {
            oldEndVnode = oldCh[--oldEndIdx]
        } else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        } else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldStartVnode, newEndVnode)) {
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
            canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
            oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
            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] : null
            if (isUndef(idxInOld)) {
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
                newStartVnode = newCh[++newStartIdx]
            } else {
                elmToMove = oldCh[idxInOld]
                if (sameVnode(elmToMove, newStartVnode)) {
                    patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
                    oldCh[idxInOld] = undefined
                    canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
                    newStartVnode = newCh[++newStartIdx]
                } else {
                    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
                    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(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
}

算了这个图没画明白,借用网上的图

oldStartIdxnewStartIdxoldEndIdx以及newEndIdx分别是新老两个VNode两边的索引,同时oldStartVnodenewStartVnodeoldEndVnodenew EndVnode分别指向这几个索引对应的vnode。整个遍历需要在oldStartIdx小于oldEndIdx并且newStartIdx小于newEndIdx(这里为了简便,称sameVnode为相似)

  1. oldStartVnode不存在的时候,oldStartVnode向右移动,oldStartIdx1
  2. oldEndVnode不存在的时候,oldEndVnode向右移动,oldEndIdx1
  3. oldStartVnodenewStartVnode相似,oldStartVnodenewStartVnode都向右移动,oldStartIdxnewStartIdx都增加1
  4. oldEndVnodenewEndVnode相似,oldEndVnodenewEndVnode都向左移动,oldEndIdxnewEndIdx都减1
  5. oldStartVnodenewEndVnode相似,则把oldStartVnode.elm移动到oldEndVnode.elm的节点后面。然后oldStartIdx向后移动一位,newEndIdx向前移动一位

6. oldEndVnodenewStartVnode相似时,把oldEndVnode.elm插入到oldStartVnode.elm前面。同样的,oldEndIdx向前移动一位,newStartIdx向后移动一位。
7. 当以上情况都不符合的时候 生成一个key与旧vnode对应的哈希表

function createKeyToOldIdx (children, beginIdx, endIdx) {
    let i, key
    const map = {}
    for (i = beginIdx; i <= endIdx; ++i) {
        key = children[i].key
        if (isDef(key)) map[key] = i
    }
    return map
}

最后生成的对象就是以childrenkey为属性,递增的数字为属性值的对象例如

children = [{
    key: 'key1'
}, {
    key: 'key2'
}]
// 最后生成的map
map = {
    key1: 0,
    key2: 1,
}

所以oldKeyToIdx就是key和旧vnodekey对应的哈希表 根据newStartVnodekey看能否找到对应的oldVnode

  • 如果oldVnode不存在,就创建一个新节点,newStartVnode向右移动
  • 如果找到节点:
    • 并且和newStartVnode相似。将map表中该位置的赋值undefined(用于保证key是唯一的)。同时将newStartVnode.elm插入啊到oldStartVnode.elm的前面,然后index向后移动一位
    • 如果不符合sameVnode,只能创建一个新节点插入到parentElm的子节点中,newStartIdx向后移动一位
  1. 结束循环后

    • oldStartIdx又大于oldEndIdx,就将新节点中没有对比的节点加到队尾中

    • 如果newStartIdx > newEndIdx,就说明还存在新节点,就将这些节点进行删除

总结

本篇文章对数据发生改变时,视图是如何更新进行了讲解。对一些细节地方进行了省略,如果需要了解更加深入,结合源码更加合适。我的github请多多关注,谢谢

Log

  • 12-19: 更新patch的具体过程
  • 1-15: 更新keep-alive的实现原理