什么是patch
在浏览器中,每次操作dom,都会引起一次重绘重排过程,如果短时间进行多次操作,对性能损耗很大,容易引起卡顿。
在vue中,使用虚拟dom(Virtual dom),来对真实dom的一种抽象化处理的树结构,模拟真实dom,提升性能。
而在更新dom节点时,通过对虚拟dom的对比diff(如果是更新操作)来进行对虚拟dom结构的增删改的一系列操作流程,就是patch过程。
前置了解
在解读源码之前,需要知道一下vnode中的几个属性以及“最长递增子序列”算法,以便对patch流程阅读更加清晰。
最长递增子序列算法
在对比数组类型的子节点时,当新节点不是旧节点的简单push新增或pop删除等操作,有移动删除新增等操作,会使用此算法算出最小操作节点组合,用最少的执行步骤完成对比。
详情请到LeetCode查看,里面有大量解题思路,最常见的解法是动态规划
而vue里面用了维基百科里的贪心+二分查找解法去做,相比动态规划,时间复杂度能更小。
动态规划:O(n^2);
贪心+二分查找:O(nlogn);
leetcode 300. 最长递增子序列
PatchFlags.FRAGMENT:
什么情况下会为此值,STABLE_FRAGMENT、KEYED_FRAGMENT、UNKEYED_FRAGMENT必定有一个fragment包裹。
1、一个组件有多个根节点,会为其创建一个包裹:STABLE_FRAGMENT
2、v-if,有多个子节点: STABLE_FRAGMENT
3、v-for语句: KEYED_FRAGMENT、UNKEYED_FRAGMENT
// 源码v-for指令创建节点时fragmentFlag变量就是判断是否使用哪个。
const fragmentFlag = isStableFragment ? PatchFlags.STABLE_FRAGMENT : keyProp ? PatchFlags.KEYED_FRAGMENT : PatchFlags.UNKEYED_FRAGMENT;
// 其余其他属性忽略,有兴趣请自行查看
interface VNode {
// 节点的key属性,被当作节点的标志,用以优化
key: string | number | symbol | null,
// 一个枚举值,是一个标识,描述该组件的类型,值是位运算左移的结果
shapeFlag: number,
// 一个枚举值,也是一个标识,描述组件的特性,帮助实现vue3中patch对比的一个特性:靶向更新
// 值大于0,即代表所对应的element在patch阶段,可以进行优化diff
// 值小于0,即代表所对应的element在patch阶段,不需要进行diff
// 重点:patchFlag可以代表多个状态组合
patchFlag: number,
// 存储子组件的变量
// 纯文本 数组 slot对象
children: string | VNodeArrayChildren | RawSlots | null
}
export const enum PatchFlags {
// 动态的文本节点
TEXT = 1,
// 动态的class节点
CLASS = 1 << 1,
// 动态的style节点
STYLE = 1 << 2,
// 动态属性节点
PROPS = 1 << 3,
// 有动态key的节点,每当key改变,需要进行完整的diff
FULL_PROPS = 1 << 4,
// 绑定了监听事件
HYDRATE_EVENTS = 1 << 5,
// 不会变换子节点顺序的fragment
STABLE_FRAGMENT = 1 << 6,
// 有带key的fragment
KEYED_FRAGMENT = 1 << 7,
// 没有带key的fragment
UNKEYED_FRAGMENT = 1 << 8,
// 一个子节点只会进行非props比较
NEED_PATCH = 1 << 9,
// 动态slot 比如带v-for的slot插槽
DYNAMIC_SLOTS = 1 << 10,
// 以下类型不会被diff
// 开发阶段注释文本,不需要diff
DEV_ROOT_FRAGMENT = 1 << 11,
// 静态节点,不需要diff
HOISTED = -1,
// 用来表示一个节点不需要优化模式optimized,patch时进行全比对
BAIL = -2
}
export const enum ShapeFlags {
// 普通html节点
ELEMENT = 1,
// 函数组件
FUNCTIONAL_COMPONENT = 1 << 1,
// 普通有状态组件
STATEFUL_COMPONENT = 1 << 2,
// 子组件是纯文本
TEXT_CHILDREN = 1 << 3,
// 子组件是数组列表
ARRAY_CHILDREN = 1 << 4,
// 子组件有slot插槽
SLOTS_CHILDREN = 1 << 5,
// vue3 Teleport组件,具体请查看官方文档
TELEPORT = 1 << 6,
// vue3 Supspense组件,具体请查看官方文档
SUSPENSE = 1 << 7,
// 准备被KeepAlive的组件
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
// 已经被KeepAlive的组件
COMPONENT_KEPT_ALIVE = 1 << 9,
// 函数组件或普通有状态组件
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
vnode中的dynamicChildren属性
在vnode生成过程中,如果patchFlag>0(含有动态属性、动态class、动态style、动态文案等),则会将含有动态属性的children节点映射到此属性上,进行标记,那么在diff时,就不需要全量对children遍历对比子节点,只需对dynamicChildren进行对比操作,可以减少遍历次数,加快过程。
patch函数
本文会讲出patch流程,但由于patch整体流程太过长,对于不同元素渲染会有特别处理(Fragment、自定义组件、TeleportImpl),特别处理部分不会涉及到,只总结出主要逻辑。
如有兴趣,可下载vue3源码打开packages > runtime-core > src > renderer.ts 阅读。
const patch: PatchFn = (
// 旧节点
n1,
// 新节点
n2,
// 节点容器,父节点
container,
// 要以哪个节点为标准插入进这个节点的前一个位置,为空则插入到最后
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
// optimized 是否优化vNode
// __DEV__ 开发环境
// isHmrUpdating开发热更新模式
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
// 等于就是没变化不用对比,跳过此处节点
if (n1 === n2) {
return
}
// 如果tag和key值都不一样,则删除此就节点
// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(/* 忽略参数 */)
n1 = null
}
// 是否启用统一vnode处理
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const {
type, ref, shapeFlag } = n2
switch (type) {
// 文本类型
case Text:
processText(/* 忽略参数 */)
break
// 注释类型
case Comment:
processCommentNode(/* 忽略参数 */)
break
// 静态节点,不会变化
case Static:
// 没有直接渲染,有则不用渲染,因为不会变化
if (n1 == null) {
mountStaticNode(/* 忽略参数 */)
} else if (__DEV__) {
patchStaticNode(/* 忽略参数 */)
}
break
// Fragment 类型
case Fragment:
processFragment(/* 忽略参数 */)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// 普通标签处理
processElement(/* 忽略参数 */)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 自定义组件的处理
processComponent(/* 忽略参数 */)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
// Teleport组件(vue原生组件)的处理
; (type as typeof TeleportImpl).process(/* 忽略参数 */)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// Suspense组件(vue原生组件)处理
; (type as typeof SuspenseImpl).process(/* 忽略参数 */)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${
typeof type})`)
}
}
// 设置dom的ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
// 对比新旧的tag值以及key值
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
return n1.type === n2.type && n1.key === n2.key
}
流程:
- 第一个if判断没什么可说,如果新旧节点相同,则跳过此节点,对下下一个节点。
- 第二个if,直接对比新旧的tag值以及key值,如果都不一样,则删除旧节点。
- 第三个if,判断是否为BAIL特性,是则不开启优化模式。
- 来到switch分支,首先通过节点的type属性对文本类型、注释类型、静态节点和Fragment进行特殊处理,在这里Fragment因为是一个虚拟节点vue3特性,所以实际渲染会将Fragment节点的子节点patch的container为当前的container。
- 如果不属于上述类型,则在switch default,通过节点的shapeFlag(描述该组件的类型)进行不同处理。
- 先看processElement(源码在下文放出,可滚动至下方查看),processElement函数只判断是否有旧节点,如果旧节点为空则渲染,有旧