Vue3的patchFlags超详细讲解
原文链接:https://juejin.cn/post/6858955776992968712
尤老师在去年做vue3的分享的时候,就提到过vue的patchFlags
,他在vue3中扮演了非常重要的性能优化的角色,对于想要学习vue3的同学来说,理解为什么vue3的性能很棒棒肯定是一个非常重要的课题,那么了解patchFlags
就势在必行了。
首先我们要清楚什么是patchFlags
,这并不是一个向外开放的 API,大部分情况下,普通开发者也不需要关心他,甚至可以根本不知道他的存在。因为一般来说在你的构建流程里面都已经给你做好了,所以很多同学可能压根就没听说过这个东西。
patchFlags 什么时候被用到
大部分是在编译的时候,比如 SFC 在被 vue-loader 编译的时候,或者 JSX 在被 JSX 插件编译的时候,这些编译器在编译的过程中会进行判断是否需要用到 patchFlags,以此来优化 VUE 应用更新的速度。
那么说到这里,你可能还是云里雾里,接下去咱们就直接上代码了:
// createVnode
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag: number = 0,
dynamicProps: string[] | null = null,
isBlockNode = false
): VNode {
// ...
}
vue3 的createVNode
函数的签名入上,我们可以看到第 4 个参数就是patchFlag
,他的值是一个数字,后面还有两个参数,那这两个参数呢其实也是跟patchFlag
有关的,后面我们会讲到。
为了以防有同学还不知道createVNode
函数是干嘛用的,咱们再衍生一下。相信h
函数大家肯定都知道的吧,我们通过h(ElementType, props, children)
的方式来创建一个节点,这是 vue3 的虚拟 DOM API 的最常用入口,那么他的实现如下:
export function h(type: any, propsOrChildren?: any, children?: any): VNode {
if (arguments.length === 2) {
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
// single vnode without props
if (isVNode(propsOrChildren)) {
return createVNode(type, null, [propsOrChildren]);
}
// props without children
return createVNode(type, propsOrChildren);
} else {
// omit props
return createVNode(type, null, propsOrChildren);
}
} else {
if (isVNode(children)) {
children = [children];
}
return createVNode(type, propsOrChildren, children);
}
}
可以看到,这个函数其实最终也就是调用了createVnode
,他只是对于用户输入的参数进行一些调整,所以其实对于要经过编译的开发方式,比如 SFC,或者 JSX,你在编译出来的代码里面是看不到的h
函数的,应为没必要再包一层,直接createVnode
就完事了。而且这里大家也看到了,h
函数调用createVnode
的时候是不会传任何patchFlags
的,也就是这个函数是没有经过优化的。
至于为什么这么做,那是因为patchFlags
理解起来比较难,直接写代码也会非常麻烦并且非常容易出错,出错也很难调试,所以这不是一个面向用户 API。
patchFlags 是什么
export const enum PatchFlags {
// 动态文字内容
TEXT = 1,
// 动态 class
CLASS = 1 << 1,
// 动态样式
STYLE = 1 << 2,
// 动态 props
PROPS = 1 << 3,
// 有动态的key,也就是说props对象的key不是确定的
FULL_PROPS = 1 << 4,
// 合并事件
HYDRATE_EVENTS = 1 << 5,
// children 顺序确定的 fragment
STABLE_FRAGMENT = 1 << 6,
// children中有带有key的节点的fragment
KEYED_FRAGMENT = 1 << 7,
// 没有key的children的fragment
UNKEYED_FRAGMENT = 1 << 8,
// 只有非props需要patch的,比如`ref`
NEED_PATCH = 1 << 9,
// 动态的插槽
DYNAMIC_SLOTS = 1 << 10,
// SPECIAL FLAGS -------------------------------------------------------------
// 以下是特殊的flag,不会在优化中被用到,是内置的特殊flag
// 表示他是静态节点,他的内容永远不会改变,对于hydrate的过程中,不会需要再对其子节点进行diff
HOISTED = -1,
// 用来表示一个节点的diff应该结束
BAIL = -2,
}
以上就是所有的patchFlags
,和他的名字含义一致,他就是一系列的标志,来标识一个节点该如何进行更新的。
可能有些同学不是很懂他的值CLASS = 1 << 1
啥意思,为什么要用这样的值来进行表示,那这个其实很简单,这其实是对每个 flag 使用二进制数中的某一位来表示,在以上的例子中:
TEXT = 0000000001;
CLASS = 0000000010;
STYLE = 0000000100;
// 以此类推
每个变量都至少有一位是 1,那么这么做有什么好处呢?
-
很容易进行复合,我们可以通过 TEXT | CLASS
来得到0000000011
,而这个值可以表示他即有TEXT
的特性,也有CLASS
的特性 -
方便进行对比,我们拿到一个值 FLAG
的时候,想要判断他有没有TEXT
特性,只需要FLAG & TEXT > 0
就行 -
方便扩展,在足够位数的情况下,我们新增一种特性就只需要让他左移的位数加一就不会重复
这种方式其实很常见,比如我们做一个系统的权限管理的时候也会考虑这么做,在 REACT 里面这种方式也有很多应用。
patchFlags 有什么用
首先,在createVnode
的时候,会把patchFlags
相关的参数都放到vnode
对象里面:
const vnode: VNode = {
// ... other vnode attrs
patchFlag,
dynamicProps,
dynamicChildren: null,
};
那他们啥时候被用到呢,主要是在节点被更新的时候,比如patchElement
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
const el = (n2.el = n1.el!);
let { patchFlag, dynamicChildren, dirs } = n2;
// #1426 take the old vnode's patch flag into account since user may clone a
// compiler-generated vnode, which de-opts to FULL_PROPS
patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS;
if (patchFlag > 0) {
if (patchFlag & PatchFlags.FULL_PROPS) {
// element props contain dynamic keys, full diff needed
patchProps();
// ...args
} else {
if (patchFlag & PatchFlags.CLASS) {
if (oldProps.class !== newProps.class) {
hostPatchProp(el, "class", null, newProps.class, isSVG);
}
}
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, "style", oldProps.style, newProps.style, isSVG);
}
if (patchFlag & PatchFlags.PROPS) {
// if the flag is present then dynamicProps must be non-null
const propsToUpdate = n2.dynamicProps!;
// update props
}
}
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children as string);
}
}
} else if (!optimized && dynamicChildren == null) {
// unoptimized, full diff
patchProps();
// args
}
const areChildrenSVG = isSVG && n2.type !== "foreignObject";
if (dynamicChildren) {
patchBlockChildren();
// ...args
if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
traverseStaticChildren(n1, n2);
}
} else if (!optimized) {
// full diff
patchChildren();
// ...args
}
if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1);
dirs && invokeDirectiveHook(n2, n1, parentComponent, "updated");
}, parentSuspense);
}
};
这里代码其实挺长的,我进行了一些精简,核心思想就是:会根据vnode
的patchFlag
上具有的属性来执行不同的patch
方法,如果没有patchFlag
那么就执行full diff
,也就是这里的patchProps
,那么猜也知道,patchProps
应该包含了下面大部分的单个patch
,即:
-
patchClass
-
patchStyle
-
patchEvent
-
等等
具体可以看patchProps
方法,这里就不再赘述。
patchFlags 的规则
这里我们就拿几个常见的规则跟大家讲一下patchFlags
到底是怎么用的,我们不会讲得很全,因为本质上都差不多,大家自行类比就行了。
模板:
<template>
<div>{{name}}</div>
</template>
编译:
createElement("div", null, [name], PatchFlags.TEXT);
模板:
<template>
<div :class="classNames">{{name}}</div>
</template>
编译:
createElement(
"div",
{ class: classNames },
[name],
PatchFlags.TEXT | PatchFlags.CLASS
);
模板:
<template>
<div :class="classNames" :id="id">{{name}}</div>
</template>
编译:
createElement(
"div",
{ class: classNames, id: id },
[name],
PatchFlags.TEXT | PatchFlags.CLASS | PatchFlags.PROPS,
["id"] // 标明具体那几个props是动态的
);
模板:
<template>
<div :[foo]="bar">Hello</div>
</template>
编译:
createElement("div", { [foo]: bar }, ["Hello"], PatchFlags.FULL_PROPS);
以上就是patchFlags
的核心内容了,如果你还有什么疑问,可以在评论区和我讨论。
本文首发于vue3源码解析和最佳时间,这是一个包含vue3源码解析和作者总结的的最佳实践的内容的地方,内容会持续更新,欢迎关注。