Skip to content

Component

Consider the following components:

vue
<!-- Child.vue -->
<script setup>
defineProps<{ msg: string }>();
</script>

<template>
  <p>{{ msg }}</p>
</template>
vue
<!-- Parent.vue -->
<script setup>
import Child from "./Child.vue";
</script>

<template>
  <Child msg="Hello!" />
</template>

Compilation Result and Overview

Basic Component

The compilation result of Parent.vue is as follows:

js
import { createComponent as _createComponent } from "vue/vapor";

function _sfc_render(_ctx) {
  const n0 = _createComponent(_ctx.Child, { msg: () => "Hello!" }, null, true);
  return n0;
}

createComponent takes the following arguments:

  1. comp: The component definition
  2. rawProps: Array of props
  3. slots: Array of slots
  4. singleRoot: Whether it's a single root (for fallthrough attributes)

Resolution from Assets

For globally registered components:

vue
<template>
  <Foo />
</template>
js
import {
  resolveComponent as _resolveComponent,
  createComponent as _createComponent,
} from "vue/vapor";

function _sfc_render(_ctx) {
  const _component_Foo = _resolveComponent("Foo");
  const n0 = _createComponent(_component_Foo, null, null, true);
  return n0;
}

resolveComponent resolves globally registered components.

Reading the Compiler

IR

Let's look at the CREATE_COMPONENT_NODE IR.

186export interface CreateComponentIRNode extends BaseIRNode {
187  type: IRNodeTypes.CREATE_COMPONENT_NODE
188  id: number
189  tag: string
190  props: IRProps[]
191  slots: IRSlots[]
192  asset: boolean
193  root: boolean
194  once: boolean
195}
ts
export interface CreateComponentIRNode extends BaseIRNode {
  type: IRNodeTypes.CREATE_COMPONENT_NODE;
  id: number;
  tag: string;
  props: IRProps[];
  slots: IRSlots[];
  asset: boolean;
  root: boolean;
  once: boolean;
}
  • tag: The component tag name
  • props: Array of props
  • slots: Array of slots
  • asset: Whether it's a globally registered component
  • root: Whether it's the root component
  • once: When combined with v-once

Transformer

Within transformElement, it determines whether the node is a component.

55    const isComponent = tagType === ElementTypes.COMPONENT
56    const propsResult = buildProps(
57      node,
58      context as TransformContext<ElementNode>,
59      isComponent,
60    )
61
62    ;(isComponent ? transformComponentElement : transformNativeElement)(
63      tag,
64      propsResult,
65      context as TransformContext<ElementNode>,
66    )
ts
const { tag, tagType } = node;
const isComponent = tagType === ElementTypes.COMPONENT;
const propsResult = buildProps(
  node,
  context as TransformContext<ElementNode>,
  isComponent
);

(isComponent ? transformComponentElement : transformNativeElement)(
  tag,
  propsResult,
  context as TransformContext<ElementNode>
);

In transformComponentElement:

ts
context.registerOperation({
  type: IRNodeTypes.CREATE_COMPONENT_NODE,
  id: context.reference(),
  tag,
  props: propsResult[0] ? propsResult[1] : [propsResult[1]],
  asset,
  root,
  slots: slotsResult,
  once: context.inVOnce,
});

Codegen

The genCreateComponent function handles code generation.

42export function genCreateComponent(
43  oper: CreateComponentIRNode,
44  context: CodegenContext,
45): CodeFragment[] {
46  const { vaporHelper } = context
47
48  const tag = genTag()
49  const { root, props, slots, once } = oper
50  const rawProps = genRawProps(props, context)
51  const rawSlots = genRawSlots(slots, context)
52
53  return [
54    NEWLINE,
55    `const n${oper.id} = `,
56    ...genCall(
57      vaporHelper('createComponent'),
58      tag,
59      rawProps,
60      rawSlots,
61      root ? 'true' : false,
62      once && 'true',
63    ),
64    ...genDirectivesForElement(oper.id, context),
65  ]
66
67  function genTag() {
68    if (oper.asset) {
69      return toValidAssetId(oper.tag, 'component')
70    } else {
71      return genExpression(
72        extend(createSimpleExpression(oper.tag, false), { ast: null }),
73        context,
74      )
75    }
76  }
77}
ts
export function genCreateComponent(
  oper: CreateComponentIRNode,
  context: CodegenContext
): CodeFragment[] {
  const { vaporHelper } = context;

  const tag = genTag();
  const { root, props, slots, once } = oper;
  const rawProps = genRawProps(props, context);
  const rawSlots = genRawSlots(slots, context);

  return [
    NEWLINE,
    `const n${oper.id} = `,
    ...genCall(
      vaporHelper("createComponent"),
      tag,
      rawProps,
      rawSlots,
      root ? "true" : false,
      once && "true"
    ),
    ...genDirectivesForElement(oper.id, context),
  ];

  function genTag() {
    if (oper.asset) {
      return toValidAssetId(oper.tag, "component");
    } else {
      return genExpression(
        extend(createSimpleExpression(oper.tag, false), { ast: null }),
        context
      );
    }
  }
}

Props Generation

Props are wrapped as getter functions:

ts
function genProp(prop: IRProp, context: CodegenContext, isStatic?: boolean) {
  return [
    ...genPropKey(prop, context),
    ": ",
    ...(prop.handler
      ? genEventHandler(context, prop.values[0])
      : isStatic
        ? ["() => (", ...genExpression(prop.values[0], context), ")"]
        : genExpression(prop.values[0], context)),
    // ...
  ];
}

This allows props to be lazily evaluated, enabling reactive updates.

Reading the Runtime

createComponent is implemented in apiCreateComponent.ts in runtime-vapor.

12export function createComponent(
13  comp: Component,
14  rawProps: RawProps | null = null,
15  slots: RawSlots | null = null,
16  singleRoot: boolean = false,
17  once: boolean = false,
18): ComponentInternalInstance {
19  const current = currentInstance!
20  const instance = createComponentInstance(
21    comp,
22    singleRoot ? withAttrs(rawProps) : rawProps,
23    slots,
24    once,
25  )
26  setupComponent(instance, singleRoot)
27
28  // register sub-component with current component for lifecycle management
29  current.comps.add(instance)
30
31  return instance
32}
ts
export function createComponent(
  comp: Component,
  rawProps: RawProps | null = null,
  slots: RawSlots | null = null,
  singleRoot: boolean = false,
  once: boolean = false
): ComponentInternalInstance {
  const current = currentInstance!;
  const instance = createComponentInstance(
    comp,
    singleRoot ? withAttrs(rawProps) : rawProps,
    slots,
    once
  );
  setupComponent(instance, singleRoot);

  // register sub-component with current component for lifecycle management
  current.comps.add(instance);

  return instance;
}

Processing Flow

  1. createComponentInstance: Creates the component instance
  2. withAttrs: For singleRoot, processes fallthrough attributes
  3. setupComponent: Sets up the component (props, slots, render function)
  4. current.comps.add: Registers the child component with the parent

singleRoot and Fallthrough Attributes

When singleRoot: true, attributes passed from the parent are automatically applied to the child component's root element:

ts
const instance = createComponentInstance(
  comp,
  singleRoot ? withAttrs(rawProps) : rawProps,
  slots,
  once
);

Lifecycle Management

Child components are registered in the parent's comps Set:

ts
current.comps.add(instance);

This ensures that child components are properly destroyed when the parent component unmounts.


Components in Vapor Mode support the same API as the Virtual DOM version.
Props are wrapped as getter functions, enabling reactive updates.
The details of createComponentInstance and setupComponent will be covered in a separate chapter.