数据更新时的 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);
}
}
}
同行比对
通过一个例子来模拟一下:
在设置好状态后,我们开始第一遍比较,此时oldStartVnode=a,
`newStartVnode=a;命中了
sameVnode(oldStartVnode,newStartVnode)逻辑,则直接调用
patchVnode(oldStartVnode,newStartVnode,insertedVnodeQueue)方法更新节点
a,接着把
oldStartIdx和
newStartIdx`索引分别+1,
更新完节点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,如图:
更新完节点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,如图:
更新完d后,我们开始第4遍比较,此时newStartVnode=e,节点e在旧树里是没有的,因此应该被作为一个新的元素插入,调用createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm),后面执行了nodeOps.insertBefore(parent, elm, ref)方法把e插入到c之前,接着把newStartIdx索引+1,如图:
插入节点e后,我们可以看到newStartIdx已经大于newEndIdx了,while循环已经完毕。接着调用removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) 删除旧的c,最终如图:
updateChildren通过以上几步操作完成了旧树子节点的更新,实际上只用了比较小的dom操作,在性能上有所提升,并且当子节点越复杂,这种提升效果越明显。vnode通过patch方法生成dom后,会调用mounted hook,至此,整个vue实例就创建完成了,当这个vue实例的watcher观察到数据变化时,会两次调用render方法生成新的vnode,接着调用patch方法对比新旧vnode来更新dom.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。