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.
<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:
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.
<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.
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.
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
.
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
<div>
<div>
{{ "count" }} : <span>{{ count }}</span>
</div>
</div>
part.
Extracting only the necessary parts,
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
.
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.)
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.
{
"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.
{
"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,
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.
23 childContext.reference()
This is done after transformNode
has been called on the children of this element, so the recursively entered transformChildren
processes first.
20 transformNode(childContext)
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
.
{
"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.
{
"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
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!