# vnode

Vue在2.x中引入了vnode,它是用来描述节点信息的javascript对象。在以往的版本中关于vnode并没有什么特别要分析地方, 但是在3.x版本中为了优化patch的过程,Vue在编译和创建vnode的过程中新增了很多逻辑,不仅新增了一些vnode的类型,例如 Suspense、Teleport、Fragment等等,还通过ShapeFlag和PatchFlag对vnode的类型和动态信息进行标记。通过ShapeFlag, patch过程只需要执行相应vnode类型的分支逻辑即可,减少了许多条件判断,而有了PatchFlag在组件的更新阶段只需要对比新旧vnode上动 态的地方即可,大大提高了更新效率。

Vue3.x对vnode的另一个优化就是brock tree。以往在更新组件的过程中,都要遍历整个组件的vnode tree,这就使得 组件的更新速度是和模板的大小正相关的,这对于某些存在大量静态节点的组件很不友好。而在3.x版本中,通过brock tree在创建 vnode的过程中收集模板的动态节点,在大部分情况下只需要更新组件的动态节点即可,这使得组件的更新速度变成了和模板的动态节点 正相关,从而大大提高了patch速度。

# ShapeFlag

ShapeFlags的类型有以下几种,通过左移操作符来枚举ShapeFlags的值,配合按位与和按位或操作符来添加或者判断vnode的ShapeFlags,举个例子: 当vnode是一个普通element vnode并且它有多个子节点时,它的shapeFlag属性的值为ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN = 17, 同时也可以使用按位与(vnode.shapeFlag & ShapeFlags.ELEMENT)来判断vnode是不是一个普通element vnode。

export const enum ShapeFlags {
  // vnode是一个普通element vnode
  ELEMENT = 1,
  // vnode是一个函数组件vnode
  FUNCTIONAL_COMPONENT = 1 << 1,
  // vnode是一个普通组件vnode
  STATEFUL_COMPONENT = 1 << 2,
  // vnode的子节点是普通文本
  TEXT_CHILDREN = 1 << 3,
  // vnode有多个子节点
  ARRAY_CHILDREN = 1 << 4,
  // vnode的子节点为插槽
  SLOTS_CHILDREN = 1 << 5,
  // vnode是一个teleport
  TELEPORT = 1 << 6,
  // vnode是一个suspense
  SUSPENSE = 1 << 7,
  // 当组件使用KEEPALIVE时,组件vnode还未被缓存的状态
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  // 当组件使用KEEPALIVE时,组件vnode已经被缓存的状态
  COMPONENT_KEPT_ALIVE = 1 << 9,
  // vnode是一个组件vnode
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}

# PatchFlag


通过PatchFlags来标记一个vnode包含的动态信息,在更新组件的阶段只需要对比新旧vnode的动态部分即可。

export const enum PatchFlags {
  // vnode的节点内容为动态文本
  TEXT = 1,
  // vnode绑定了动态的class
  CLASS = 1 << 1,
  // vnode绑定了动态的style
  STYLE = 1 << 2,
  // vnode绑定了除了class、style的其他props
  PROPS = 1 << 3,
  // vnode绑定了动态key值的props
  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,
  // 绑定了v-ref或者其他自定义指令
  NEED_PATCH = 1 << 9,
  // 有动态插槽的组件vnode,存在此标记时组件每次都需要强制更新
  DYNAMIC_SLOTS = 1 << 10,
  // 特殊标记
  // vnode是一个纯静态节点
  HOISTED = -1,
  // 特殊标记
  // 存在此标记时在diff阶段需要跳过优化模式
  // 例如非编译器生成的插槽,通过手写render函数生成的vnode等
  BAIL = -2
}

# Vue 3 Template Explorer

Vue官方提供了一个在线编译器Template Explorer,用来在线预览模板编译成的渲染函数。 我们可以通过Template Explorer来看看模板编译成的渲染函数究竟是什么样的:

<template>
  <div>TEXT{{state}}</div>
  <div :class="state">CLASS</div>
  <div :style="state">STYLE</div>
  <div :aa="state">PROPS</div>
  <div :[state2]="state">FULL PROPS</div>
  <div v-for="item in list">UNKEYED_FRAGMENT</div>
  <div v-for="item in list" :key="item.key">KEYED_FRAGMENT</div>
  <div v-ref="state">NEED_PATCH</div>
  <Child>
    <template v-slot:[state]="dynamic">
      DYNAMIC_SLOTS
    </template>
  </Child>
  <div>HOISTED</div>
</template>
(_openBlock(), _createBlock(_Fragment, null, [
  _createVNode("div", null, "TEXT" + _toDisplayString(_ctx.state), 1 /* TEXT */),
  _createVNode("div", { class: _ctx.state }, "CLASS", 2 /* CLASS */),
  _createVNode("div", { style: _ctx.state }, "STYLE", 4 /* STYLE */),
  _createVNode("div", { aa: _ctx.state }, "PROPS", 8 /* PROPS */, ["aa"]),
  _createVNode("div", { [_ctx.state2]: _ctx.state }, "FULL PROPS", 16 /* FULL_PROPS */),
  (_openBlock(true), _createBlock(_Fragment, null, _renderList(_ctx.list, (item) => {
    return (_openBlock(), _createBlock("div", null, "UNKEYED_FRAGMENT"))
  }), 256 /* UNKEYED_FRAGMENT */)),
  (_openBlock(true), _createBlock(_Fragment, null, _renderList(_ctx.list, (item) => {
    return (_openBlock(), _createBlock("div", {
      key: item.key
    }, "KEYED_FRAGMENT"))
  }), 128 /* KEYED_FRAGMENT */)),
  _withDirectives(_createVNode("div", null, "NEED_PATCH", 512 /* NEED_PATCH */), [
    [_directive_ref, _ctx.state]
  ]),
  _createVNode(_component_Child, null, {
    [_ctx.state]: _withCtx((dynamic) => [
      _createTextVNode(" DYNAMIC_SLOTS ")
    ]),
    _: 2
  }, 1024 /* DYNAMIC_SLOTS */),
  _createVNode("div", null, "HOISTED")
], 64 /* STABLE_FRAGMENT */))

以上的模板基本包含了所有vnode的PatchFlag类型,这里可以注意一下Vue3.x版本已经支持模板有多个根节点,在这种情况下Vue会自动生成一个Stable Fragment节点作为vnode tree的根节点。

# 创建vnode以及收集动态节点的过程

<!-- bock1 -->
<div>
  <!-- bock2 -->
  <div v-if="state">
    <div>
      <span>静态节点</span>
      <span>动态节点1{{state}}</span>
    </div>
  </div>
  <!-- bock3 -->
  <div v-for="item in state">
  </div>
  <div>
    <span>静态节点</span>
    <span>动态节点2{{state}}</span>
  </div>
</div>
(_openBlock(), _createBlock("div", null, [
  (_ctx.state)
    ? (_openBlock(), _createBlock("div", { key: 0 }, [
        _createVNode("div", null, [
          _createVNode("span", null, "静态节点"),
          _createVNode("span", null, _toDisplayString(_ctx.state), 1 /* TEXT */)
        ])
      ]))
    : _createCommentVNode("v-if", true),
  (_openBlock(true), _createBlock(_Fragment, null, _renderList(_ctx.state, (item) => {
    return (_openBlock(), _createBlock("div", null, " bock2 "))
  }), 256 /* UNKEYED_FRAGMENT */)),
  _createVNode("div", null, [
    _createVNode("span", null, "静态节点"),
    _createVNode("span", null, _toDisplayString(_ctx.state), 1 /* TEXT */)
  ])
]))

我们以上面的模板生成的渲染函数为例子来分析执行渲染函数创建vnode的过程。在Vue3.x中,Vue会将模板分成一个个block,用于收集 动态节点。模板的根节点、有v-for或者v-if指令的节点都是一个block,一个block内的所有动态节点都会添加到block节点生成的 vnode的dynamicChildren属性上,在组件更新的时候只需要对比所有动态节点即可。从上面生成的渲染函数可以看到,在执行_createVNode函数生成vnode时,根节点、有v-for或v-if指令的节点在创建vnode之前都会调用_openBlock和_createBlock打开并 创建一个block:

// openBlock根据传入的disableTracking参数来设置当前的currentBlock
// 并将当前的currentBlock添加到blockStack栈中
// 从上面的渲染函数可以看到,如果是v-for生成的block,这里传入的disableTracking是true,currentBlock = null
// 跟openBlock对应的还有closeBlock
export function openBlock(disableTracking = false) {
  blockStack.push((currentBlock = disableTracking ? null : []))
}

export function closeBlock() {
  blockStack.pop()
  currentBlock = blockStack[blockStack.length - 1] || null
}

根据以上渲染函数,首先调用_openBlock打开根节点block1,然后先从子节点开始创建vnode,此时子节点恰好存在v-if指令, 所以会再调用一次_openBlock打开block2,此时blockStack = [[], []] currentBlock = [],currentBlock收集的 为block2的动态节点,然后开始创建block2的子节点:


// 创建vnode
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 {
  ...

  // 根据传入的type来确定shapeFlag值
  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

  // 创建一个vnode对象,保存vnode相关的信息
  const vnode: VNode = {
    __v_isVNode: true,
    [ReactiveFlags.SKIP]: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    children: null,
    component: null,
    suspense: null,
    dirs: null,
    transition: null,
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
    staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null,
    appContext: null
  }

  // 标准化子节点
  // 根据子节点的类型添加shapeFlag
  normalizeChildren(vnode, children)

  // 判断当前节点是否应该添加到当前的block中
  // 1、如果shouldTrack > 0,在某些情况下不应该将vnode添加当前的blcok中,此时会将shouldTrack设置为-1
  // 例如存在v-noce指令的节点及其子节点
  // 2、当前节点不是block节点
  // 3、patchFlag > 0或者节点是一个组件节点,也就是存在动态信息的节点
  // 4、patchFlag不等于PatchFlags.HYDRATE_EVENTS,这个patchFlag属于ssr相关,这里暂时不分析
  if (
    shouldTrack > 0 &&
    // avoid a block node from tracking itself
    !isBlockNode &&
    // has current parent block
    currentBlock &&
    // presence of a patch flag indicates this node needs patching on updates.
    // component nodes also should always be patched, because even if the
    // component doesn't need to update, it needs to persist the instance on to
    // the next vnode so that it can be properly unmounted later.
    (patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
    // the EVENTS flag is only for hydration and if it is the only flag, the
    // vnode should not be considered dynamic due to handler caching.
    patchFlag !== PatchFlags.HYDRATE_EVENTS
  ) {
    currentBlock.push(vnode)
  }

  return vnode
}

当创建完block2的子节点后,会调用_createBlock方法创建block2节点。此时currentBlock添加了block2的动态节点


export function createBlock(
  type: VNodeTypes | ClassComponent,
  props?: Record<string, any> | null,
  children?: any,
  patchFlag?: number,
  dynamicProps?: string[]
): VNode {
  // 调用createVNode创建block2节点的vnode
  // 注意这里传入的最后一个参数isBlockNode为true
  // 所以block节点不会添加到currentBlock中
  const vnode = createVNode(
    type,
    props,
    children,
    patchFlag,
    dynamicProps,
    true /* isBlock: prevent a block from tracking itself */
  )
  // 将block2下所有的动态节点也就是currentBlock保存到block2节点vnode的dynamicChildren属性上
  // 这里要注意的是,如果当前创建的是是v-for的block vnode
  // 那么currentBlock = null,此时不管v-for里面有没有动态节点dynamicChildren都是[]
  // 因为v-for产生的block的子节点都是动态的,所以会默认更新所有子节点
  vnode.dynamicChildren = currentBlock || EMPTY_ARR
  // 调用closeBlock,关闭当前blcok,并且将保存当前block2动态节点的currentBlock出栈
  // 将parent block,也就是block1的动态节点重新赋值给currentBlock
  closeBlock()

  // 当前block2节点的vnode也是它的父block节点的动态节点
  // 所以将当前节点添加到父block的动态节点中
  if (currentBlock) {
    currentBlock.push(vnode)
  }
  return vnode
}

其他block的创建都是相同的过程,当执行完整个渲染函数之后,当前的block tree为:

{
  name: '根vnode(block1)'
  dynamicChildren: [
    {
      name: 'v-if vnode(block2)',
      dynamicChildren: [
        {
          name: '动态节点1',
          dynamicChildren: null
        }
      ]
    },
    {
      name: 'v-for vnode(block3),
      dynamicChildren: []
    },
    {
      name: '动态节点2',
      dynamicChildren: null
    }
  ]
}

通过分析vnode的创建过程,我们了解了Vue3.x中对于vnode的优化以及block tree的创建过程,这些优化会大大提升 组件更新时的效率,接下来我们就继续分析组件更新的过程,看看Vue如何利用优化过的vnode来提升组件更新的效率。