前面几章路过 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
的过程就大致分析完了。