1

数据更新时的 diff 和 patch 机制

一、数据视图更新

在当响应式数据发生变化的时候,就会触发setter ,对应的Dep中的watcher也会触发,watcher对象就会调用update方法,最终将新产生的VNode节点和老的VNode节点进行一个patch过程,对比得到差异之后,最终将这些差异更新到视图上。

二、 patch

首先说一下 patch 的核心 diff 算法,我们用 diff 算法可以比对出两颗树的「差异」

diff 算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有 O(n),是一种相当高效的算法,如下图。

function patch (oldVNode, vnode, parentElm) {
    if (!oldVnode) {
        // 如果不存在旧节点,就相当于是在新的节点替代原本没有的节点
        addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
    } else if (!vnode) {
        // 如果新的节点中没有,那么就相当于是删除了原来的节点
        removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
    } else {
        // 如果都存在
        if (sameVnode(oldVNode, vnode)) {
            // 新旧的节点是相同
            patchVnode(oldVNode, vnode);
        } else {
            // 新旧的节点是不相同的
            // 删除老节点,增加新节点。
            removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
            addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
        }
    }
}
同行比对

通过一个例子来模拟一下:

clipboard.png

在设置好状态后,我们开始第一遍比较,此时oldStartVnode=a,`newStartVnode=a;命中了sameVnode(oldStartVnode,newStartVnode)逻辑,则直接调用patchVnode(oldStartVnode,newStartVnode,insertedVnodeQueue)方法更新节点a,接着把oldStartIdxnewStartIdx`索引分别+1,

clipboard.png

更新完节点a后,我们开始第2遍比较,此时oldStartVnode=b,newEndVnode=b;命中了sameVnode(oldStartVnode,newEndVnode)逻辑,则调用patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)方法更新节点b,接着调用canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)),把节点b移到树的最右边,最后把oldStartIdx索引+1,newEndIdx索引-1,如图:

clipboard.png

更新完节点b后,我们开始第三遍比较,此时oldEndVnode=d,newStartVnode=d;命中了sameVnode(oldEndVnode, newStartVnode)逻辑,则调用patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)方法更新节点d,接着调用canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm),把d移到c的左边。最后把oldEndIdx索引-1,newStartIdx索引+1,如图:

clipboard.png

更新完d后,我们开始第4遍比较,此时newStartVnode=e,节点e在旧树里是没有的,因此应该被作为一个新的元素插入,调用createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm),后面执行了nodeOps.insertBefore(parent, elm, ref)方法把e插入到c之前,接着把newStartIdx索引+1,如图:

clipboard.png

插入节点e后,我们可以看到newStartIdx已经大于newEndIdx了,while循环已经完毕。接着调用removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) 删除旧的c,最终如图:

clipboard.png

updateChildren通过以上几步操作完成了旧树子节点的更新,实际上只用了比较小的dom操作,在性能上有所提升,并且当子节点越复杂,这种提升效果越明显。vnode通过patch方法生成dom后,会调用mounted hook,至此,整个vue实例就创建完成了,当这个vue实例的watcher观察到数据变化时,会两次调用render方法生成新的vnode,接着调用patch方法对比新旧vnode来更新dom.


Meils
1.6k 声望157 粉丝

前端开发实践者