简介

VNode是vue用来描述 DOM 的 JavaScript 对象,它在vue中可以描述不同类型的节点,比如元素节点、组件节点等。

示例

我们在html中的dom节点是这样的

  1. <div class="className">Hello World</div>

而在vue中的vnode是这样的

const vnode = {
  type: 'div',
  props: { 
    'class': 'className',
  },
  children: ['Hello World']
}

如果是一个组件,就是这样的:

<div>
  <MyComponent></MyComponent>
</div>
const elementVNode = {
  tag: 'div',
  props: null,
  children: [{
    tag: MyComponent,
    props: null
  }]
}

区分VNode类型

在vue的vnode中,新增了一个字段 shapeFlag,它是一个枚举常量。

export const enum ShapeFlags {
  /** HTML 或 SVG 标签 普通 DOM 元素 */
  ELEMENT = 1, 
  /** 函数式组件 */
  FUNCTIONAL_COMPONENT = 1 << 1,
  /** 普通有状态组件 */
  STATEFUL_COMPONENT = 1 << 2,
  /** 子节点是纯文本 */
  TEXT_CHILDREN = 1 << 3,
  /** 子节点是数组 */
  ARRAY_CHILDREN = 1 << 4,
  /** 子节点是插槽 */
  SLOTS_CHILDREN = 1 << 5,
  /**  Teleport   */
  TELEPORT = 1 << 6,
  /**  Suspense */
  SUSPENSE = 1 << 7,
  /** 需要被 keep-alive 的有状态组件 */
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  /** 已经被 keep-alive 的有状态组件 */
  COMPONENT_KEPT_ALIVE = 1 << 9,
  /** 有状态组件和函数组件都是组件,用 COMPONENT 表示 */
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}

如何创建VNode

在看源码之前,有必要提前讲解一个概念:
在3.0中提出了一个新的概率 Block Tree,顾名思义就是块状区域树。为了解决2.0版本一直被人诟病的diff低效问题;
每个block下,都有一个 dynamicChildren ,在vnode / block创建阶段会将当前block子区域内的动态内容收集并填充到dynamicChildren,那么整个render函数执行完毕时,每个block下的动态子代内容就都被收集到各自的dynamicChildren中,在正式diff时就不再需要重新遍历整棵树,只需要比对同级block下dynamicChildren,无差别的深度遍历升级为靶向的同级比较,这就做到了只关注动态内容。

创建 Block

(openBlock(), createBlock(type, props, children .... ))

源码:

// block 栈
const blockStack = []    
// 当前处于收集态的仓库
let currentBlock = null

// 用 openBlock 来存储当前block的动态内容收集栈
function openBlock(disableTracking = false) {
  blockStack.push((currentBlock = disableTracking ? null : []))
}

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

这里的 openBlock 一定要在 createBlock 之前调用,要保证在创建子节点前创建一个block收集仓库。

function createBlock(type, props, children, patchFlag, dynamicProps) {
  const vnode = createVNode(
    type,
    props,
    children,
    patchFlag,
    dynamicProps,
    true /* isBlock 声明这里是一个block,下面有解释 */
  )
  // 此时子节点的动态内容已经收集完毕,将收集的内容挂载到对应的block vnode上
  vnode.dynamicChildren = currentBlock || []
  // 收集完毕后将当前收集仓库做失效处理
  closeBlock()
  // block作为一个动态块状子区域需要被作为动态子节点收集到父级block对应的仓库中
  if (shouldTrack$1 > 0 && currentBlock) {
    currentBlock.push(vnode)
  }
  return vnode
}

创建普通VNode

我们在挂载原理的文章中会发现有这样的代码:

mount(rootContainer, isHydrate) {
    ...
  const vnode = createVNode(rootComponent, rootProps)
  // rootComponent 就是createApp传入的option
}

下面我们来看createVNode的源码:

function _createVNode(
    type,
    props = null,
    children = null,
    patchFlag = 0,
    dynamicProps = null,
    isBlockNode = false
  ) {
    if (isVNode(type)) {
      // 已经是一个vnode了。这种情况发生在
      // <component :is="vnode"/>
      // 请确保在克隆过程中合并引用,而不是覆盖它
      const cloned = cloneVNode(type, props, true /* mergeRef: true */)
      if (children) {
        normalizeChildren(cloned, children)
      }
      return cloned
    }
    // 如果是一个class组件
    if (isClassComponent(type)) {
      type = type.__vccOpts
    }
    // class 和 style 的标准化,处理在vue中 class=['a'] class={} 等多种情况
    if (props) {
      if (isProxy(props) || InternalObjectKey in props) {
        props = extend({}, props)
      }
      let { class: klass, style } = props
      if (klass && !isString(klass)) {
        props.class = normalizeClass(klass)
      }
      if (isObject(style)) {
        if (isProxy(style) && !isArray(style)) {
          style = extend({}, style)
        }
        props.style = normalizeStyle(style)
      }
    }
    // 标记vnode的类型信息
    const shapeFlag = isString(type)
      ? 1 /* ELEMENT */
      : isSuspense(type)
      ? 128 /* SUSPENSE */
      : isTeleport(type)
      ? 64 /* TELEPORT */
      : isObject(type)
      ? 4 /* STATEFUL_COMPONENT */
      : isFunction(type)
      ? 2 /* FUNCTIONAL_COMPONENT */
      : 0
    if (shapeFlag & 4 /* STATEFUL_COMPONENT */ && isProxy(type)) {
      type = toRaw(type)
    }

    // 这里就是vnode的结构了
    const vnode = {
      __v_isVNode: true,
      ['__v_skip' /* SKIP */]: true,
      type,
      props,
      key: props && normalizeKey(props),
      ref: props && normalizeRef(props),
      scopeId: currentScopeId,
      children: null,
      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,
    }
       // 标准化子节点,其实就是整合子元素
    normalizeChildren(vnode, children)

    // suspense的话需要单独处理
    if (shapeFlag & 128 /* SUSPENSE */) {
      const { content, fallback } = normalizeSuspenseChildren(vnode)
      vnode.ssContent = content
      vnode.ssFallback = fallback
    }

    if (
      shouldTrack$1 > 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 & 6) /* 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 !== 32 /* HYDRATE_EVENTS */
    ) {
      // 动态子代节点或子代block收集到父级block中
      currentBlock.push(vnode)
    }

    return vnode
  }

总结:

  1. 判断如果已经是vnode类型了,返回一个clone的vnode;
  2. 如果是class组件,赋值type;
  3. 对props和style的标准化处理,因为在vue中class的写法有很多种,例 :class=”[‘a’]” :class={active: true} ;
  4. 标记vnode的 shapeFlag;
  5. 制定vnode的数据结构;
  6. 判断当前动态子节点block是否收集到父级block中;