vue3 VNode

vue3 VNode

Posted by ZeFeng on March 3, 2021

本文是 Vue 3.0 进阶系列 的第五篇文章,在这篇文章中,将介绍 Vue 3 中的核心对象 —— VNode,该对象用于描述节点的信息,它的全称是虚拟节点(virtual node)。与 “虚拟节点” 相关联的另一个概念是 “虚拟 DOM”,它是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。通常一个 Vue 应用会以一棵嵌套的组件树的形式来组织:

(图片来源:https://v3.cn.vuejs.org/)

所以 “虚拟 DOM” 对 Vue 应用来说,是至关重要的。而 “虚拟 DOM” 又是由 VNode 组成的,它是 Vue 底层的核心基石。接下来,将带大家一起来探索 Vue 3 中与 VNode 相关的一些知识。

一、VNode 长什么样?

// packages/runtime-core/src/vnode.ts  
export interface VNode<  
  HostNode = RendererNode,  
  HostElement = RendererElement,  
  ExtraProps = { [key: string]: any }  
> {  
 // 省略内部的属性  
}  

runtime-core/src/vnode.ts 文件中,我们找到了 VNode 的类型定义。通过 VNode 的类型定义可知,VNode 本质是一个对象,该对象中按照属性的作用,分为 5 大类。这里只详细介绍其中常见的两大类型属性 —— 内部属性DOM 属性

1.1 内部属性

__v_isVNode: true // 标识是否为VNode  
[ReactiveFlags.SKIP]: true // 标识VNode不是observable  
type: VNodeTypes // VNode 类型  
props: (VNodeProps & ExtraProps) | null // 属性信息  
key: string | number | null // 特殊 attribute 主要用在 Vue 的虚拟 DOM 算法  
ref: VNodeNormalizedRef | null // 被用来给元素或子组件注册引用信息。  
scopeId: string | null // SFC only  
children: VNodeNormalizedChildren // 保存子节点  
component: ComponentInternalInstance | null // 指向VNode对应的组件实例  
dirs: DirectiveBinding[] | null // 保存应用在VNode的指令信息  
transition: TransitionHooks<HostElement> | null // 存储过渡效果信息  

1.2 DOM 属性

el: HostNode | null // element   
anchor: HostNode | null // fragment anchor  
target: HostElement | null // teleport target  
targetAnchor: HostNode | null // teleport target anchor  
staticCount: number // number of elements contained in a static vnode  

1.3 suspense 属性

suspense: SuspenseBoundary | null  
ssContent: VNode | null  
ssFallback: VNode | null  

1.4 optimization 属性

shapeFlag: number  
patchFlag: number  
dynamicProps: string[] | null  
dynamicChildren: VNode[] | null  

1.5 应用上下文属性

appContext: AppContext | null  

二、如何创建 VNode?

要创建 VNode 对象的话,我们可以使用 Vue 提供的 h 函数。也许可以更准确地将其命名为 createVNode(),但由于频繁使用和简洁,它被称为 h() 。该函数接受三个参数:

// packages/runtime-core/src/h.ts  
export function h(type: any, propsOrChildren?: any, children?: any): VNode {  
  const l = arguments.length  
  if (l === 2) {   
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {   
      // single vnode without props  
      if (isVNode(propsOrChildren)) {  
        return createVNode(type, null, [propsOrChildren])  
      }  
      // 只包含属性不含有子元素  
      return createVNode(type, propsOrChildren) // h('div', { id: 'foo' })  
    } else {  
      // 忽略属性  
      return createVNode(type, null, propsOrChildren) // h('div', ['foo'])  
    }  
  } else {  
    if (l > 3) {  
      children = Array.prototype.slice.call(arguments, 2)  
    } else if (l === 3 && isVNode(children)) {  
      children = [children]  
    }  
    return createVNode(type, propsOrChildren, children)  
  }  
}  

观察以上代码可知, h 函数内部的主要处理逻辑就是根据参数个数和参数类型,执行相应处理操作,但最终都是通过调用 createVNode 函数来创建 VNode 对象。在开始介绍 createVNode 函数前,先举一些实际开发中的示例:

const app = createApp({ // 示例一  
  render: () => h('div', '我')  
})  
  
const Comp = () => h("p", "我"); // 示例二  
  
app.component('component-a', { // 示例三  
  template: "<p>我</p>"  
})  

示例一和示例二很明显都使用了 h 函数,而示例三并未看到 hcreateVNode 函数的身影。为了一探究竟,我们需要借助 Vue 3 Template Explorer 这个在线工具来编译一下 "<p>我是</p>" 模板,该模板编译后的结果如下(函数模式):

// https://vue-next-template-explorer.netlify.app/  
const _Vue = Vue  
return function render(_ctx, _cache, $props, $setup, $data, $options) {  
  with (_ctx) {  
    const { createVNode: _createVNode, openBlock: _openBlock,  
      createBlock: _createBlock } = _Vue  
    return (_openBlock(), _createBlock("p", null, "我"))  
  }  
}  

由以上编译结果可知, "<p>我</p>" 模板被编译生成了一个 render 函数,调用该函数后会返回 createBlock 函数的调用结果。其中 createBlock 函数的实现如下所示:

// packages/runtime-core/src/vnode.ts  
export function createBlock( type: VNodeTypes | ClassComponent,  
  props?: Record<string, any> | null,  
  children?: any,  
  patchFlag?: number,  
  dynamicProps?: string[]): VNode {  
  const vnode = createVNode(  
    type,  
    props,  
    children,  
    patchFlag,  
    dynamicProps,  
    true /* isBlock: prevent a block from tracking itself */  
  )  
  // 省略部分代码  
  return vnode  
}  

createBlock 函数内部,我们终于看到了 createVNode 函数的身影。顾名思义,该函数的作用就是用于创建 VNode,接下来我们来分析一下它。

三、createVNode 函数内部做了啥?

下面我们将从参数说明和逻辑说明两方面来介绍 createVNode 函数:

3.1 参数说明

createVNode 被定义在 runtime-core/src/vnode.ts 文件中:

// packages/runtime-core/src/vnode.ts  
export const createVNode = (__DEV__  
  ? createVNodeWithArgsTransform  
  : _createVNode) as typeof _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 {  
  //   
  return vnode  
}  

在分析该函数的具体代码前,我们先来看一下它的参数。该函数可以接收 6 个参数,这里用思维导图来重点介绍前面 2 个参数:

type 参数
// packages/runtime-core/src/vnode.ts  
function _createVNode( type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,  
  // 省略其他参数): VNode { ... }  

由上图可知,type 参数支持很多类型,比如常用的 stringVNodeComponent 等。此外,也有一些陌生的面孔,比如 TextCommentStaticFragment 等类型,它们的定义如下:

// packages/runtime-core/src/vnode.ts  
export const Text = Symbol(__DEV__ ? 'Text' : undefined)  
export const Comment = Symbol(__DEV__ ? 'Comment' : undefined)  
export const Static = Symbol(__DEV__ ? 'Static' : undefined)  
  
export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as {  
  __isFragment: true  
  new (): {  
    $props: VNodeProps  
  }  
}  

那么定义那么多的类型有什么意义呢?这是因为在 patch 阶段,会根据不同的 VNode 类型来执行不同的操作:

// packages/runtime-core/src/renderer.ts  
function baseCreateRenderer( options: RendererOptions,  
  createHydrationFns?: typeof createHydrationFunctions): any {  
  const patch: PatchFn = (  
    n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null,  
    isSVG = false, optimized = false  
  ) => {  
    // 省略部分代码  
    const { type, ref, shapeFlag } = n2  
    switch (type) {  
      case Text: // 处理文本节点  
        processText(n1, n2, container, anchor)  
        break  
      case Comment: // 处理注释节点  
        processCommentNode(n1, n2, container, anchor)  
        break  
      case Static: // 处理静态节点  
        if (n1 == null) {  
          mountStaticNode(n2, container, anchor, isSVG)  
        } else if (__DEV__) {  
          patchStaticNode(n1, n2, container, isSVG)  
        }  
        break  
      case Fragment: // 处理Fragment节点  
        processFragment(...)  
        break  
      default:  
        if (shapeFlag & ShapeFlags.ELEMENT) { // 元素类型  
          processElement(...)  
        } else if (shapeFlag & ShapeFlags.COMPONENT) { // 组件类型  
          processComponent(...)  
        } else if (shapeFlag & ShapeFlags.TELEPORT) { // teleport内置组件  
          ;(type as typeof TeleportImpl).process(...)  
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {  
          ;(type as typeof SuspenseImpl).process(...)  
        }  
    }  
  }  
}  

介绍完 type 参数后,接下来我们来看 props 参数,具体如下图所示:

props 参数
function _createVNode( type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,  
  props: (Data & VNodeProps) | null = null,): VNode { ... }  

props 参数的类型是联合类型,这里我们来分析 Data & VNodeProps 交叉类型:

其中 Data 类型是通过 TypeScript 内置的工具类型 Record 来定义的:

export type Data = Record<string, unknown>  
type Record<K extends keyof any, T> = {  
  [P in K]: T;  
};  

VNodeProps 类型是通过类型别名来定义的,除了含有 keyref 属性之外,其他的属性主要是定义了与生命周期有关的钩子:

// packages/runtime-core/src/vnode.ts  
export type VNodeProps = {  
  key?: string | number  
  ref?: VNodeRef  
  
  // vnode hooks  
  onVnodeBeforeMount?: VNodeMountHook | VNodeMountHook[]  
  onVnodeMounted?: VNodeMountHook | VNodeMountHook[]  
  onVnodeBeforeUpdate?: VNodeUpdateHook | VNodeUpdateHook[]  
  onVnodeUpdated?: VNodeUpdateHook | VNodeUpdateHook[]  
  onVnodeBeforeUnmount?: VNodeMountHook | VNodeMountHook[]  
  onVnodeUnmounted?: VNodeMountHook | VNodeMountHook[]  
}  

3.2 逻辑说明

createVNode 函数内部涉及较多的处理逻辑,这里我们只分析主要的逻辑:

// 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 {  
  // 处理VNode类型,比如处理动态组件的场景:<component :is="vnode"/>  
  if (isVNode(type)) {  
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)  
    if (children) {  
      normalizeChildren(cloned, children)  
    }  
    return cloned  
  }  
  
  // 类组件规范化处理  
  if (isClassComponent(type)) {  
    type = type.__vccOpts  
  }  
  
  // 类和样式规范化处理  
  if (props) {  
    // 省略相关代码  
  }  
  
  // 把vnode的类型信息转换为位图  
  const shapeFlag = isString(type)  
    ? ShapeFlags.ELEMENT // ELEMENT = 1  
    : __FEATURE_SUSPENSE__ && isSuspense(type)  
      ? ShapeFlags.SUSPENSE // SUSPENSE = 1 << 7,  
      : isTeleport(type)  
        ? ShapeFlags.TELEPORT // TELEPORT = 1 << 6,  
        : isObject(type)  
          ? ShapeFlags.STATEFUL_COMPONENT // STATEFUL_COMPONENT = 1 << 2,  
          : isFunction(type)  
            ? ShapeFlags.FUNCTIONAL_COMPONENT // FUNCTIONAL_COMPONENT = 1 << 1,  
            : 0  
  
  // 创建VNode对象  
  const vnode: VNode = {  
    __v_isVNode: true,  
    [ReactiveFlags.SKIP]: true,  
    type,  
    props,  
    // ...  
  }  
  
  // 子元素规范化处理  
  normalizeChildren(vnode, children)  
  return vnode  
}  

介绍完 createVNode 函数之后,再来介绍另一个比较重要的函数 —— normalizeVNode

四、如何创建规范的 VNode 对象?

normalizeVNode 函数的作用,用于将传入的 child 参数转换为规范的 VNode 对象。

// packages/runtime-core/src/vnode.ts  
export function normalizeVNode(child: VNodeChild): VNode {  
  if (child == null || typeof child === 'boolean') { // null/undefined/boolean -> Comment  
    return createVNode(Comment)  
  } else if (isArray(child)) { // array -> Fragment  
    return createVNode(Fragment, null, child)  
  } else if (typeof child === 'object') { // VNode -> VNode or mounted VNode -> cloned VNode  
    return child.el === null ? child : cloneVNode(child)  
  } else { // primitive types:'foo' or 1  
    return createVNode(Text, null, String(child))  
  }  
}  

由以上代码可知,normalizeVNode 函数内部会根据 child 参数的类型进行不同的处理:

4.1 null / undefined -> Comment

expect(normalizeVNode(null)).toMatchObject({ type: Comment })  
expect(normalizeVNode(undefined)).toMatchObject({ type: Comment })  

4.2 boolean -> Comment

expect(normalizeVNode(true)).toMatchObject({ type: Comment })  
expect(normalizeVNode(false)).toMatchObject({ type: Comment })  

4.3 array -> Fragment

expect(normalizeVNode(['foo'])).toMatchObject({ type: Fragment })  

4.4 VNode -> VNode

const vnode = createVNode('div')  
expect(normalizeVNode(vnode)).toBe(vnode)  

4.5 mounted VNode -> cloned VNode

const mounted = createVNode('div')  
mounted.el = {}  
const normalized = normalizeVNode(mounted)  
expect(normalized).not.toBe(mounted)  
expect(normalized).toEqual(mounted)  

4.6 primitive types

expect(normalizeVNode('foo')).toMatchObject({ type: Text, children: `foo` })  
expect(normalizeVNode(1)).toMatchObject({ type: Text, children: `1` })  

五、有话说

5.1 如何判断是否为 VNode 对象?

// packages/runtime-core/src/vnode.ts  
export function isVNode(value: any): value is VNode {  
  return value ? value.__v_isVNode === true : false  
}  

VNode 对象中含有一个 __v_isVNode 内部属性,利用该属性可以用来判断当前对象是否为 VNode 对象。

5.2 如何判断两个 VNode 对象的类型是否相同?

// packages/runtime-core/src/vnode.ts  
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {  
  // 省略__DEV__环境的处理逻辑  
  return n1.type === n2.type && n1.key === n2.key  
}  

在 Vue 3 中,是通过比较 VNode 对象的 typekey 属性,来判断两个 VNode 对象的类型是否相同。

5.3 如何快速创建某些类型的 VNode 对象?

在 Vue 3 内部提供了 createTextVNodecreateCommentVNodecreateStaticVNode 函数来快速的创建文本节点、注释节点和静态节点:

createTextVNode
export function createTextVNode(text: string = ' ', flag: number = 0): VNode {  
  return createVNode(Text, null, text, flag)  
}  
createCommentVNode
export function createCommentVNode( text: string = '',  
  asBlock: boolean = false): VNode {  
  return asBlock  
    ? (openBlock(), createBlock(Comment, null, text))  
    : createVNode(Comment, null, text)  
}  
createStaticVNode
export function createStaticVNode( content: string,  
  numberOfNodes: number): VNode {  
  const vnode = createVNode(Static, null, content)  
  vnode.staticCount = numberOfNodes  
  return vnode  
}  

本文主要介绍了 VNode 对象是什么、如何创建 VNode 对象及如何创建规范的 VNode 对象。为了让大家能够更深入地理解 hcreateVNode 函数的相关知识,还从源码的角度分析了 createVNode 函数 。

在后续的文章中,将会介绍 VNode 在 Vue 3 内部是如何被使用的,感兴趣的小伙伴不要错过哟。

六、参考资源

  • Vue 3 官网 - 渲染函数