Vue3 源码解析系列四-创建虚拟节点

319 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

回顾

上一篇讲到了从 createApp 这条线下去找,找到了 app 的属性,然后知道了在 mount 里面调用createVNode创建了根节点的 vnode,最后调用了render方法,对这个vnode 进行了渲染。 这篇我们来分析一下 createVNode

createVNode

createVNode 的位置在 /packages/runtime-core/src/vnode.ts

// packages/runtime-core/src/vnode.ts
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 {
  if (isVNode(type)) {
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)
    if (children) {
      normalizeChildren(cloned, children)
    }
    return cloned
  }

  // class & style normalization.
  if (props) {
    // for reactive or proxy objects, we need to clone it to enable mutation.
    props = guardReactiveProps(props)!
    let { class: klass, style } = props
    if (klass && !isString(klass)) {
      props.class = normalizeClass(klass)
    }
    if (isObject(style)) {
      // reactive state objects need to be cloned since they are likely to be
      // mutated
      if (isProxy(style) && !isArray(style)) {
        style = extend({}, style)
      }
      props.style = normalizeStyle(style)
    }
  }

  // encode the vnode type information into a bitmap
  
  // 注意 type 有可能是 string 也有可能是对象
  // 如果是对象的话,那么就是用户设置的 options
  // type 为 string 的时候
  // createVNode("div")
  // type 为组件对象的时候
  // createVNode(App)
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT
    : __FEATURE_SUSPENSE__ && isSuspense(type)
    ? ShapeFlags.SUSPENSE
    : isTeleport(type)
    ? ShapeFlags.TELEPORT
    : isObject(type)
    ? ShapeFlags.STATEFUL_COMPONENT
    : isFunction(type)
    ? ShapeFlags.FUNCTIONAL_COMPONENT
    : 0

  return createBaseVNode(
    type,
    props,
    children,
    patchFlag,
    dynamicProps,
    shapeFlag,
    isBlockNode,
    true
  )
}

函数中初始化了一些数据,并且对传入的组件做了一些处理,如果传入的组件已经是一个 VNode 节点,则直接对其克隆并返回。 通过 type 确定了shapeFlag属性,该属性标识了该VNode的类型,最后调用了createBaseVNode方法。

// packages/runtime-core/src/vnode.ts
function createBaseVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag = 0,
  dynamicProps: string[] | null = null,
  shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
  isBlockNode = false,
  needFullChildrenNormalization = false
) {
  const vnode = {
    __v_isVNode: true,
    __v_skip: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    slotScopeIds: null,
    children,
    component: null,
    suspense: null,
    ssContent: null,
    ssFallback: null,
    dirs: null,
    transition: null,
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
    staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null,
    appContext: null
  } as VNode

  if (needFullChildrenNormalization) {
    normalizeChildren(vnode, children)
  } else if (children) {
    vnode.shapeFlag |= isString(children)
      ? ShapeFlags.TEXT_CHILDREN
      : ShapeFlags.ARRAY_CHILDREN
  }

  return vnode
}

这个方法真正的生成了 VNode 对象,并对子节点进行了一些处理。
所以这块的流程是 createVNode => _createVNode => createBaseVNode => VNode

ShapeFlag

我们接下来看下 VNode 的 ShapeFlag 标志。

export const enum ShapeFlags {
  ELEMENT = 1,
  FUNCTIONAL_COMPONENT = 1 << 1,
  STATEFUL_COMPONENT = 1 << 2,
  TEXT_CHILDREN = 1 << 3,
  ARRAY_CHILDREN = 1 << 4,
  SLOTS_CHILDREN = 1 << 5,
  TELEPORT = 1 << 6,
  SUSPENSE = 1 << 7,
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  COMPONENT_KEPT_ALIVE = 1 << 9,
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}

我们注意到 ShapeFlag 基本都是通过十进制数字 1 进行左位移操作不同的位数得到的。然后根据其中部分属性派生出了 COMPONENT 类型, 因为有状态组件 和 函数式组件都是“组件”,用 COMPONENT 表示。

那为什么这里要用位操作来当作标识符呢?有什么好处?
我们先来感受一些,后续代码中是如何通过这些标识符判断的。

if (shapeFlag & ShapeFlags.ELEMENT) {

} else if (shapeFlag & ShapeFlags.COMPONENT) {
  
} else if (shapeFlag & ShapeFlags.TELEPORT) {
  
}

可以看到这里也采用位运算来判断种类,因为在一次挂载任务中这种判断很可能大量的进行,使用位运算在一定程度上再次拉升了运行时性能,相比而言,位掩码的运算速度远比直接判断 === 运算的高,除却函数调用带来额外开销,位运算发生于系统底层。
所以使用位操作的好处就是效率更高。
我们把 ShapeFlags 整理成表格,这样就能很清楚的理解为什么可以用位操作符 & 来判断。

ShapeFlags左移运算32位的bit序列
ELEMENT0000000001
FUNCTIONAL_COMPONENT1 << 10000000010
STATEFUL_COMPONENT1 << 20000000100
TEXT_CHILDREN1 << 30000001000
ARRAY_CHILDREN1 << 40000010000
SLOTS_CHILDREN1 << 50000100000
TELEPORT1 << 60001000000
SUSPENSE1 << 70010000000
COMPONENT_SHOULD_KEEP_ALIVE1 << 80100000000
COMPONENT_KEPT_ALIVE1 << 91000000000

而根据 FUNCTIONAL_COMPONENT 和 STATEFUL_COMPONENT 的值我们可以得到 COMPONENT

ShapeFlags左移运算32位的bit序列
COMPONENT0000001 10

所以可以看出只有 ShapeFlags.FUNCTIONAL_COMPONENT 和 ShapeFlags.STATEFUL_COMPONENT 与 ShapeFlags.COMPONENT 进行位与(&)运算,才会得到非零值,即为真。

小结

这节我们分析了VNode的生成,并研究了为什么 VNode 的标识符使用位操作进行判断。