Skip to content

Complex Templates

Since I explained the scheduler in between, the order is somewhat out of sequence, but let's continue looking at Vapor's components.
Currently, we have come to understand a simple template that contains the following mustache.

vue
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>

<template>
  <p>{{ count }}</p>
</template>

Now, regarding the template part of this, what happens if we write something a little more complex?
Currently, it is simply compiled to:

ts
import {
  renderEffect as _renderEffect,
  setText as _setText,
  template as _template,
} from "vue/vapor";

const t0 = _template("<p></p>");

function _sfc_render(_ctx) {
  const n0 = t0();
  _renderEffect(() => _setText(n0, _ctx.count));
  return n0;
}

But what happens if we nest elements more or make the mustache partial? (e.g., count: {{ count }})
Let's take a look at the following component as an example.

vue
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>

<template>
  <p>count is {{ count }}</p>
  <div>
    <div>
      {{ "count" }} : <span>{{ count }}</span>
    </div>
  </div>
</template>

Compilation Result

The compilation result is as follows.
Since the script part is the same, it is omitted.

js
import {
  createTextNode as _createTextNode,
  prepend as _prepend,
  renderEffect as _renderEffect,
  setText as _setText,
  template as _template,
} from "vue/vapor";

const t0 = _template("<p></p>");
const t1 = _template("<div><div><span></span></div></div>");

function _sfc_render(_ctx) {
  const n0 = t0();
  const n4 = t1();
  const n3 = n4.firstChild;
  const n2 = n3.firstChild;
  const n1 = _createTextNode(["count", " : "]);
  _prepend(n3, n1);
  _renderEffect(() => {
    _setText(n0, "count is ", _ctx.count);
    _setText(n2, _ctx.count);
  });
  return [n0, n4];
}

Understanding the Overview

First, since the template of this component is a fragment, two templates are generated, and it returns two resulting nodes.

js
const t0 = _template("<p></p>");
const t1 = _template("<div><div><span></span></div></div>");

function _sfc_render(_ctx) {
  const n0 = t0();
  const n4 = t1();
  // Omitted
  return [n0, n4];
}

As you can see from t0 and t1, the text has been removed from the template, leaving only the elements.
And for n0, everything is inserted using setText.

ts
const t0 = _template("<p></p>");
const t1 = _template("<div><div><span></span></div></div>");

function _sfc_render(_ctx) {
  const n0 = t0();
  const n4 = t1();
  // Omitted
  _renderEffect(() => {
    _setText(n0, "count is ", _ctx.count);
    // Omitted
  });
  return [n0, n4];
}

Up to here, you should be able to understand the implementation seen so far.\

The problem is the compilation of the

vue
<div>
  <div>
    {{ "count" }} : <span>{{ count }}</span>
  </div>
</div>

part.
Extracting only the necessary parts,

js
const t1 = _template("<div><div><span></span></div></div>");

function _sfc_render(_ctx) {
  const n4 = t1();
  const n3 = n4.firstChild;
  const n2 = n3.firstChild;
  const n1 = _createTextNode(["count", " : "]);
  _prepend(n3, n1);
  _renderEffect(() => {
    _setText(n2, _ctx.count);
  });

  // Omitted
}

First, what we can see is that the count : part is generated by createTextNode and inserted before the firstChild of n4.

js
const t1 = _template("<div><div><span></span></div></div>");
const n4 = t1();
const n3 = n4.firstChild;
const n1 = _createTextNode(["count", " : "]);
_prepend(n3, n1);

And the <span></span> part is inserted using setText.
It's a bit confusing, so let's add comments indicating which Node corresponds to which.
(To make it easier to understand, I have assigned IDs to the elements.)

js
const t0 = _template("<p></p>");
const t1 = _template(
  "<div id='root'><div id='inner'><span></span></div></div>"
);

function _sfc_render(_ctx) {
  const n0 = t0(); // p
  const n4 = t1(); // div#root
  const n3 = n4.firstChild; // div#inner
  const n2 = n3.firstChild; // span
  const n1 = _createTextNode(["count", " : "]); // "count" :
  _prepend(n3, n1); // append `"count : "` to pre of `div#inner`
  _renderEffect(() => {
    _setText(n0, "count is ", _ctx.count); // set `count is ${_ctx.count}` to p;
    _setText(n2, _ctx.count); // set `${_ctx.count}` to span
  });
  return [n0, n4];
}

Indeed, this seems to hold.
The parts of the compiler's implementation that we want to confirm this time are as follows:

  • Implementation that outputs code to access firstChild when nested.
  • Implementation that outputs createTextNode.
  • Implementation that outputs inserting elements with prepend.

Reading the Compiler (Transformer)

Well, it's okay to read from AST again, but let's change things up and reverse-engineer from IR for a change.
You should have gotten used to it by now, so you should be able to roughly understand IR as well.

json
{
  "type": "IRRoot",
  "source": "\n  <p>count is {{ count }}</p>\n  <div>\n    <div>\n      {{ 'count' }} : <span>{{ count }}</span>\n    </div>\n  </div>\n",
  "template": ["<p></p>", "<div><div><span></span></div></div>"],
  "component": {},
  "directive": {},
  "block": {
    "type": "IRBlock",
    "dynamic": {
      "flags": 1,
      "children": [
        {
          "flags": 1,
          "children": [
            {
              "flags": 3,
              "children": []
            },
            {
              "flags": 3,
              "children": []
            }
          ],
          "id": 0,
          "template": 0
        },
        {
          "flags": 1,
          "children": [
            {
              "flags": 1,
              "children": [
                {
                  "flags": 7,
                  "children": [],
                  "id": 1
                },
                {
                  "flags": 3,
                  "children": []
                },
                {
                  "flags": 1,
                  "children": [
                    {
                      "flags": 3,
                      "children": []
                    }
                  ],
                  "id": 2
                }
              ],
              "id": 3
            }
          ],
          "id": 4,
          "template": 1
        }
      ]
    },
    "effect": [
      {
        "expressions": [
          {
            "type": "SimpleExpression",
            "content": "count",
            "isStatic": false,
            "constType": 0,
            "ast": null
          }
        ],
        "operations": [
          {
            "type": "IRSetText",
            "element": 0,
            "values": [
              {
                "type": "SimpleExpression",
                "content": "count is ",
                "isStatic": true,
                "constType": 3
              },
              {
                "type": "SimpleExpression",
                "content": "count",
                "isStatic": false,
                "constType": 0,
                "ast": null
              }
            ]
          },
          {
            "type": "IRSetText",
            "element": 2,
            "values": [
              {
                "type": "SimpleExpression",
                "content": "count",
                "isStatic": false,
                "constType": 0,
                "ast": null
              }
            ]
          }
        ]
      }
    ],
    "operation": [
      {
        "type": "IRCreateTextNode",
        "id": 1,
        "values": [
          {
            "type": "SimpleExpression",
            "content": "'count'",
            "isStatic": false,
            "constType": 0,
            "ast": {
              "type": "StringLiteral",
              "start": 1,
              "end": 8,
              "extra": {
                "rawValue": "count",
                "raw": "'count'",
                "parenthesized": true,
                "parenStart": 0
              },
              "value": "count",
              "comments": [],
              "errors": []
            }
          },
          {
            "type": "SimpleExpression",
            "content": " : ",
            "isStatic": true,
            "constType": 3
          }
        ],
        "effect": false
      },
      {
        "type": "IRPrependNode",
        "elements": [1],
        "parent": 3
      }
    ],
    "returns": [0, 4]
  }
}

It is quite long, but if you read it calmly, you should understand.

First of all, obviously, there is an IRNode. The template part where the block's children are fragments has id 0 and 4 respectively.
The template property also has the template's id.

json
{
  "type": "IRRoot",
  "template": ["<p></p>", "<div><div><span></span></div></div>"],
  "block": {
    "type": "IRBlock",
    "dynamic": {
      "flags": 1,
      "children": [
        {
          "id": 0,
          "template": 0
        },
        {
          "id": 4,
          "template": 1
        }
      ]
    }
  }
}

At this point, in codegen,

js
const t0 = _template("<p></p>");
const t1 = _template(
  "<div id='root'><div id='inner'><span></span></div></div>"
);

const n0 = t0();
const n4 = t1();

can be generated.

Why the id suddenly jumps to 4 is because it delves into the children and climbs from the inside out.
The id for transformChildren was generated at the following timing.

This is done after transformNode has been called on the children of this element, so the recursively entered transformChildren processes first.

In other words, the generation of id increments from the leaf nodes to the parent.
This time, by chance, the child Nodes of t1 are #inner, span, and the Text inside it, so they are assigned id 3, 2, and 1 respectively (since 0 is obtained from t0), and the Node obtained from t1 is assigned 4.

json
{
  "type": "IRRoot",
  "template": [
    "<p></p>",
    "<div id='root'><div id='inner'><span></span></div></div>"
  ],
  "block": {
    "type": "IRBlock",
    "dynamic": {
      "children": [
        // p
        {
          "id": 0,
          "template": 0
        },

        // #root
        {
          "id": 4,
          "template": 1,
          "children": [
            // #inner
            {
              "id": 3,
              "children": [
                // Text
                { "id": 1 },

                // span
                { "id": 2 }
              ]
            }
          ]
        }
      ]
    }
  }
}

Let's also take a look at the IR for the operation and effect parts.

json
{
  "effect": [
    {
      "expressions": [
        {
          "type": "SimpleExpression",
          "content": "count",
          "isStatic": false,
          "constType": 0,
          "ast": null
        }
      ],
      "operations": [
        {
          "type": "IRSetText",
          "element": 0,
          "values": [
            {
              "type": "SimpleExpression",
              "content": "count is ",
              "isStatic": true,
              "constType": 3
            },
            {
              "type": "SimpleExpression",
              "content": "count",
              "isStatic": false,
              "constType": 0,
              "ast": null
            }
          ]
        },
        {
          "type": "IRSetText",
          "element": 2,
          "values": [
            {
              "type": "SimpleExpression",
              "content": "count",
              "isStatic": false,
              "constType": 0,
              "ast": null
            }
          ]
        }
      ]
    }
  ],
  "operation": [
    {
      "type": "IRCreateTextNode",
      "id": 1,
      "values": [
        {
          "type": "SimpleExpression",
          "content": "'count'",
          "isStatic": false,
          "constType": 0,
          "ast": {
            "type": "StringLiteral",
            "start": 1,
            "end": 8,
            "extra": {
              "rawValue": "count",
              "raw": "'count'",
              "parenthesized": true,
              "parenStart": 0
            },
            "value": "count",
            "comments": [],
            "errors": []
          }
        },
        {
          "type": "SimpleExpression",
          "content": " : ",
          "isStatic": true,
          "constType": 3
        }
      ],
      "effect": false
    },
    {
      "type": "IRPrependNode",
      "elements": [1],
      "parent": 3
    }
  ]
}

Let's also take a look at the IR for the operation and effect parts.

Effect has two IRSetText, and operation has IRCreateTextNode and IRPrependNode.
If these are generated, codegen can be performed.

SetText should be fine.
You can follow the transformText part we've seen so far.

Let's look at IRCreateTextNode.
This is also generated by transformText.

45function processTextLike(context: TransformContext<InterpolationNode>) {
46  const nexts = context.parent!.node.children.slice(context.index)
47  const idx = nexts.findIndex(n => !isTextLike(n))
48  const nodes = (idx > -1 ? nexts.slice(0, idx) : nexts) as Array<TextLike>
49
50  const id = context.reference()
51  const values = nodes.map(node => createTextLikeExpression(node, context))
52
53  context.dynamic.flags |= DynamicFlag.INSERT | DynamicFlag.NON_TEMPLATE
54
55  context.registerOperation({
56    type: IRNodeTypes.CREATE_TEXT_NODE,
57    id,
58    values,
59    effect: !values.every(isConstantExpression) && !context.inVOnce,
60  })
61}

This processTextLike passes through the second branch, which is when there is an INTERPOLATION.

29  if (
30    node.type === NodeTypes.ELEMENT &&
31    node.tagType === ElementTypes.ELEMENT &&
32    isAllTextLike(node.children)
33  ) {
34    processTextLikeContainer(
35      node.children,
36      context as TransformContext<ElementNode>,
37    )
38  } else if (node.type === NodeTypes.INTERPOLATION) {
39    processTextLike(context as TransformContext<InterpolationNode>)
40  } else if (node.type === NodeTypes.TEXT) {

Finally, IRPrependNode.
The part where PREPEND_NODE is registered is as follows.

68        } else {
69          context.registerOperation({
70            type: IRNodeTypes.PREPEND_NODE,
71            elements: prevDynamics.map(child => child.id!),
72            parent: context.reference(),
73          })
74        }

Where is this? It is in a function called processDynamicChildren, within the processing of transformChildren, and it is called when isFragment is falsy.
It takes out each child from children and collects Nodes that have the DynamicFlag.INSERT set.
This time, as can be seen from,

45function processTextLike(context: TransformContext<InterpolationNode>) {
46  const nexts = context.parent!.node.children.slice(context.index)
47  const idx = nexts.findIndex(n => !isTextLike(n))
48  const nodes = (idx > -1 ? nexts.slice(0, idx) : nexts) as Array<TextLike>
49
50  const id = context.reference()
51  const values = nodes.map(node => createTextLikeExpression(node, context))
52
53  context.dynamic.flags |= DynamicFlag.INSERT | DynamicFlag.NON_TEMPLATE
54
55  context.registerOperation({
56    type: IRNodeTypes.CREATE_TEXT_NODE,
57    id,
58    values,
59    effect: !values.every(isConstantExpression) && !context.inVOnce,
60  })
61}

this flag is set.

We have understood that these processes transform into IR.

Reading the Codegen

As long as the IR is done, codegen is nothing difficult, so let's take a quick look.

The new parts are mainly IRCreateTextNode and IRPrependNode.

33export function genOperation(
34  oper: OperationNode,
35  context: CodegenContext,
36): CodeFragment[] {
54    case IRNodeTypes.CREATE_TEXT_NODE:
55      return genCreateTextNode(oper, context)
28export function genCreateTextNode(
29  oper: CreateTextNodeIRNode,
30  context: CodegenContext,
31): CodeFragment[] {
32  const { vaporHelper } = context
33  const { id, values, effect } = oper
34  return [
35    NEWLINE,
36    `const n${id} = `,
37    ...genCall(vaporHelper('createTextNode'), [
38      effect && '() => ',
39      ...genMulti(
40        DELIMITERS_ARRAY,
41        ...values.map(value => genExpression(value, context)),
42      ),
43    ]),
44  ]
45}

58    case IRNodeTypes.PREPEND_NODE:
59      return genPrependNode(oper, context)
22export function genPrependNode(
23  oper: PrependNodeIRNode,
24  { vaporHelper }: CodegenContext,
25): CodeFragment[] {
26  return [
27    NEWLINE,
28    ...genCall(
29      vaporHelper('prepend'),
30      `n${oper.parent}`,
31      ...oper.elements.map(el => `n${el}`),
32    ),
33  ]
34}

Code generation for accessing firstChild is handled in genChildren with conditional branches.
firstChild is somewhat special, otherwise, it outputs executing a helper function children.

63    } else {
64      if (newPaths.length === 1 && newPaths[0] === 0) {
65        push(`n${from}.firstChild`)
66      } else {
67        push(
68          ...genCall(
69            vaporHelper('children'),
70            `n${from}`,
71            ...newPaths.map(String),
72          ),
73        )
74      }
75    }

genChildren recursively executes genChildren while passing around from and id.

Reading the Runtime

ts
const t0 = _template("<p></p>");
const t1 = _template(
  "<div id='root'><div id='inner'><span></span></div></div>"
);

function _sfc_render(_ctx) {
  const n0 = t0(); // p
  const n4 = t1(); // div#root
  const n3 = n4.firstChild; // div#inner
  const n2 = n3.firstChild; // span
  const n1 = _createTextNode(["count", " : "]); // "count" :
  _prepend(n3, n1); // append `"count : "` to pre of `div#inner`
  _renderEffect(() => {
    _setText(n0, "count is ", _ctx.count); // set `count is ${_ctx.count}` to p;
    _setText(n2, _ctx.count); // set `${_ctx.count}` to span
  });
  return [n0, n4];
}

The runtime code is not much of a big deal, so let's go through it quickly.

createTextNode

First, createTextNode.

It really just does document.createTextNode.
It receives either an array of values or a getter function.
In the case of a getter, it is considered dynamic and wrapped with renderEffect.

39export function createTextNode(values?: any[] | (() => any[])): Text {
40  // eslint-disable-next-line no-restricted-globals
41  const node = document.createTextNode('')
42  if (values)
43    if (isArray(values)) {
44      setText(node, ...values)
45    } else {
46      renderEffect(() => setText(node, ...values()))
47    }
48  return node
49}

prepend

prepend simply calls ParentNode.prepend.

31export function prepend(parent: ParentNode, ...blocks: Block[]): void {
32  parent.prepend(...normalizeBlock(blocks))
33}

It was quite quick, but up to here, you can understand how a somewhat complex template is handled.
Knowledge of following these children and prepending elements is the same when attaching event handlers in the future.
Now, let's look at various directives and component features with a simple structure!