15

在这篇文章深入源码学习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存在,则插入到父元素上
        • 如果组件根节点被替换,遍历更新父节点element。然后移除老节点
    • 调用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

    1. 真实节点调用vnode = new VNode(tag, data, children, undefined, undefined, context)
    2. 组件节点调用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

    1. oldEndVnodenewEndVnode相似,oldEndVnodenewEndVnode都向左移动,oldEndIdxnewEndIdx都减1

    1. oldStartVnodenewEndVnode相似,则把oldStartVnode.elm移动到oldEndVnode.elm的节点后面。然后oldStartIdx向后移动一位,newEndIdx向前移动一位

    1. oldEndVnodenewStartVnode相似时,把oldEndVnode.elm插入到oldStartVnode.elm前面。同样的,oldEndIdx向前移动一位,newStartIdx向后移动一位。

    1. 当以上情况都不符合的时候

    生成一个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,就将新节点中没有对比的节点加到队尾中
    ![](https://user-gold-cdn.xitu.io/2019/11/19/16e83a83366194d3?w=784&h=373&f=png&s=73559)
    - 如果`newStartIdx > newEndIdx`,就说明还存在新节点,就将这些节点进行删除
    
    ![](https://user-gold-cdn.xitu.io/2019/11/19/16e83a871c34ea5f?w=836&h=367&f=png&s=77933)

    总结

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

    Log

    • 12-19: 更新patch的具体过程

    云中歌
    1.1k 声望121 粉丝

    哈哈哈哈