0%

vue3 原理分析之 patch 过程

vue3 patch 过程

1
vue 版本:v3.0.2

patch 方法的入口

patch 方法的签名:

1
patch(n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false)

patch 方法中主要根据新的 vnode 节点的 typeshapeFlag 分别调用相对应的方法.

patch 方法源码节选:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
switch (type) {
case Text:
Symbol processText(n1, n2, container, anchor);
break;
case Comment:
processCommentNode(n1, n2, container, anchor);
break;
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG);
}
else if ((process.env.NODE_ENV !== 'production')) {
patchStaticNode(n1, n2, container, isSVG);
}
break;
case Fragment:
processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
break;
default:
if (shapeFlag & 1 /* ELEMENT */) {
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
}
else if (shapeFlag & 6 /* COMPONENT */) {
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
}
else if (shapeFlag & 64 /* TELEPORT */) {
type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized, internals);
}
else if ( shapeFlag & 128 /* SUSPENSE */) {
type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized, internals);
}
else if ((process.env.NODE_ENV !== 'production')) {
warn('Invalid VNode type:', type, `(${typeof type})`);
}
}

processText

处理文本节点,判断旧节点是否存在,不存在则创建并插入文本节点,否则从旧节点中获取对应的 dom 元素并设置为新节点的文本。

processCommentNode

处理注释节点,判断旧节点是否存在,不存在则创建并插入注释节点,否则从旧节点中获取对应的 dom 元素设置到新节点上。

Static

静态节点在不同的环境中有一些变化:
生产环境中首次挂载直接在父元素中插入静态节点。以后更新不进行改变,显示了静态节点的不可变。
非生产环境如果新旧节点不一样,会删除旧节点并插入新节点。新旧节点不一样的情况可能会发生在手动改变静态节点上。

processFragment

vue2 中组件模板必须在一个根元素中,vue3 取消了这样的限制,通过在多个根元素上包裹一个 Fragement 类型的 vnode 节点实现,而且不会在 dom 中生成对应的元素,只会在父元素中插入 Framement 的子元素。如果只存在一个根元素不会创建 Framement

该方法会在父元素的首尾分别插入一个文本节点,用来在父元素中插入 Fragment 的 子元素时起定位的作用。然后如果是首次挂载则调用 mountChildren 方法,否则调用 patchChildren 方法。

processElement

首次挂载调用 mountElement,否则调用 patchElement

  • mountElement 主要做了 vnode 节点相对应 dom 元素的创建、插入和 vnode 的相关 hook 的调用,eg. vnode 挂载 hookvnode 上的指令的创建、挂载 hook
  • patchElement 主要做了新旧 vnodepropspatch 和根据新节点是否有动态子节点分别调用 patchBlockChildrenpatchChildren

    TELEPORT

    调用 teleportprocess 方法。同样分首次挂载与更新。
  • 首次挂载根据 prop.to 获取目标元素,调用 mountChildren 在其上插入 teleport 的子节点。
  • 更新根据是否有动态子节点调用 patchBlockChildrenpatchChildrenteleport 可以设置禁用,如果禁用则移动到未使用teleport 一样的位置。

    SUSPENSE

    调用 suspenseprocess 方法。首次挂载调用 mountSuspense,更新调用 patchSuspensesuspense 在异步依赖就绪后才会实际渲染相关组件。
  • mountSuspense 中,设置 suspense.pendingBranch 为异步组件,使异步组件在一个未插入到文档中的 dom 元素中 patchpatch 过程中会执行异步请求,异步请求就绪后才会执行实际的组件挂载 ;在异步就绪之前,先渲染 fallback,并设置 suspense.activeBranchfallback
  • patchSuspense 中如果新旧 suspensesameVnode 则则执行 patch(old, new, ...), 否则卸载旧 suspense, 执行 patch(null, new, ...)patch 逻辑与 mountSuspense 基本一致。

patchBlockChildren

遍历每个子 vnode,如果子 vnodetypeFragementComponentteleport,或新旧子节点不为相同的类型,则需要获取它们实际的父节点, 因为可能需要在父节点中进行子节点的增删、替换等操作。最后调用 patch 方法。

patchChildren

根据 vnodepatchFlagshapeFlag 区分 keyedChildrenunKeydChildren,分别调用 patchKeyedChildrenpatchUnkeyedChildren,以及新旧节点的有无调用 mountChildrenunmountChildren

patchUnkeyedChildren

依次对子节点进行 patch, 最后如果还有多余的旧子节点则进行卸载,新增的新子节点则进行挂载。

patchKeyedChildren

  • 首节点循环比对
  • 尾节点循环比对
  • 多余的旧子节点进行卸载,新增的新子节点进行挂载
  • 创建新子节点的 keymap, 查找是否有旧子节点的 key 与 新子节点的 key 相同,如果有相同的 key,记录找到的索引,否则查找是否有相同类型的新旧子节点,有则记录找到的索引,没有则卸载当前的旧节点。然后 patch; 最后如果有找到的索引,则移动节点至相应的位置,否则挂载新节点。

vue2 的不同

  • vue2 在一个大循环中进行节点比对,vue3 把每个阶段的比对放在各自的过程中
  • vue3 没有进行新旧子节点首尾的交叉比对;
  • vue3 对非相同 key 但相同类型的节点的 dom 进行了复用
  • vue2 以旧子节点为基础进行 patch 与 节点的移动,vue3 以新子节点为基础进行 patch 与 节点的移动。