你和大神的距离可能就在这里,Vue3的patchFlags超详细讲解

4,990 阅读5分钟

这是vue3源码解析在掘金上的第二篇分享,上一篇是关于vue3的响应式数据的,中间间距时间还是有点长的,但我还是会努力把这个系列做完整,还希望大家多多关注支持!

尤老师在去年做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,那么这么做有什么好处呢?

  1. 很容易进行复合,我们可以通过TEXT | CLASS来得到0000000011,而这个值可以表示他即有TEXT的特性,也有CLASS的特性
  2. 方便进行对比,我们拿到一个值FLAG的时候,想要判断他有没有TEXT特性,只需要FLAG & TEXT > 0就行
  3. 方便扩展,在足够位数的情况下,我们新增一种特性就只需要让他左移的位数加一就不会重复

这种方式其实很常见,比如我们做一个系统的权限管理的时候也会考虑这么做,在 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);
  }
};

这里代码其实挺长的,我进行了一些精简,核心思想就是:会根据vnodepatchFlag上具有的属性来执行不同的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源码解析和作者总结的的最佳实践的内容的地方,内容会持续更新,欢迎关注。