前面几章路过 patch 函数的时候都是点到为止,现在好好分析一下。
1 | return function patch(oldVnode, vnode, hydrating, removeOnly) { |
分析 patch 方法,两个 vnode 进行对比,结果无非就是三种情况。
- 新的不存在,表示要删除旧节点
- 老的不存在,表示要新增节点
都存在,进行更新,这里又分成了两种情况:
3.1 不是真实节点,且新旧节点的是同一类型的节点(根据 key 和 tag 等来判断)
3.2 其他情况,比如根组件首次渲染
场景 3.2 其他情况,比如根组件首次渲染
我们用下面这个例子先来调试下 3.2 这种场景:
1 | <div id="demo"> |
第一次进入到 patch 这个函数的时候是根组件挂载时,此时因为 oldVnode 为 demo 这个真实的元素,我们会走到这里:
1 | if (isRealElement) { |
这一段的工作包括:
- 将真实节点转为虚拟节点
- 得到旧节点的父元素
- 通过
vnode创建真实的节点并插入到旧节点的后面,所以有一瞬间会同时存在两个id为demo的div。

然后,跳过中间 isDef(vnode.parent) 这一段,我们来到:
1 | if (isDef(parentElm)) { |
这里会执行 removeVnodes 把旧的元素给删除掉,就不过多展开了。
然后,我们回过头来看看 createElm 具体是怎么实现的吧:
1 | function createElm( |
这里首先创建了一个 tag 类型的元素,并赋值给 vnode.elm。因为传进来的 vnode 是原生标签,所以最后会走到:
1 | createChildren(vnode, children, insertedVnodeQueue) |
其中 createChildren 中又调用了 createElm:
1 | function createChildren(vnode, children, insertedVnodeQueue) { |
这样不停递归地调用 createElm, 最后执行 insert(parentElm, vnode.elm, refElm) 的时候,vnode.elm 就是一颗完整的 dom 树了,执行完 insert 以后,这颗树就插入到了 body 之中。
场景 2 老的不存在,表示要新增节点
我们可以通过下面这个例子来调试一下:
1 | <div id="demo"> |
这个例子中,我们只需要关注第二次进入 patch 的流程,即自定义组件的挂载过程。因为之前组件化渲染流程已经说过,自定义组件在 $mount 的时候也会走到 patch 之中,不过,这时因为旧的节点并不存在,所以会走到:
1 | if (isUndef(oldVnode)) { |
createElm 函数上面已经介绍过了,后面的逻辑就是一样的了。
场景 3.1 不是真实节点,且新旧节点的是同一类型的节点
接下来就是我们的重头戏了,我们先看看这个例子:
1 | <div id="demo"> |
我们在定时器中对 name 进行了重新赋值,此时会触发组件的更新,最终走到 patch 函数:
1 | ... |
我们去掉一些我们暂时不关心的代码,看看 patchVnode:
1 | function patchVnode( |
这里有两个工作:
- 更新属性
- 更新
children或者更新文本
其中,更新属性的代码是平台相关的,比如浏览器中相关的代码在 src/platforms/web/runtime/modules 中,这一块暂时不是我们的重点,我们先略过。
文本节点的更新我们暂时不讨论,我们先看看 children 的更新,它分为几种情况:
- 新旧节点都有孩子,进行孩子的更新
- 新的有孩子,旧的没有孩子,进行批量添加
- 新的没有孩子,旧的有孩子,进行批量删除
其中,新增和删除都比较简单,这里就暂时先不讨论。
我们要着重分析的是 updateChildren, 它的主要作⽤是⽤⼀种较⾼效的⽅式⽐对新旧两个 vnode 的 children 得出最⼩操作补丁。执⾏⼀个双循环是传统⽅式,vue 中针对 web 场景特点做了特别的算法优化。

在新⽼两组 vnode 节点的左右头尾两侧都有⼀个变量标记,在遍历过程中这⼏个变量都会向中间靠拢。当 oldStartIdx > oldEndIdx 或者 newStartIdx > newEndIdx 时结束循环。以下是遍历规则:
⾸先,oldStartIdx、oldEndIdx 与 newStartIdx、newEndIdx 两两交叉⽐较,共有 4 种情况:
oldStartIdx与newStartIdx所对应的 node 是sameVnode:

oldEndIdx与newEndIdx所对应的 node 是sameVnode:

oldStartIdx与newEndIdx所对应的 node 是sameVnode:

这种情况不光要进行两者的 patchVNode,还需要将旧的节点移到 oldEndIdx 后面。
oldEndIdx与newStartIdx所对应的 node 是sameVnode:

同样,这种情况不光要进行两者的 patchVNode,还需要将旧的节点移到 oldStartIdx 前面。
如果四种情况都不匹配,就尝试从旧的 children 中找到一个 sameVnode,这里又分成两种情况:
- 找到了

这种情况首先进行两者的 patchVNode,然后将旧的节点移到 oldStartIdx 前面。
- 没找到
这种情况首先会通过 newStartIdx 指向的 vnode 创建一个新的元素,然后插入到 oldStartIdx 前面。

最后,如果新旧子节点中有任何一方遍历完了,还需要做一个收尾工作,这里又分为两种情况:
- 旧的先遍历完

这种情况需要将新的 children 中未遍历的节点进行插入,插入的位置后面看源码可以看到。
- 新的先遍历完

这种情况需要将旧的 children 中未遍历的节点进行删除。
规则清楚了,再看代码就很简单了:
1 | function updateChildren( |
到此,整个 patch 的过程就大致分析完了。