複雑なテンプレート
間にスケジューラの説明をしてしまったのでやや順番は前後しますが,改めて Vapor のコンポーネントについて続きを見ていきます.
今,私たちは以下のような単純にマスタッシュを持つテンプレートを理解できるようになりました.
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<p>{{ count }}</p>
</template>
さて,これの template 部分についてですが,もう少し複雑なものを書いてみるとどうなるでしょうか.
今は単純に,
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;
}
にコンパイルされていますが,もっと要素をネストさせてみたり,マスタッシュを部分的なもの (e.g. count: {{ count }}
) にしてみるとどうなるでしょう.
試しに以下のようなコンポーネントを見ていきます.
<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>
コンパイル結果
コンパイル結果は以下のようになりました.
script 部分は同じなので省略しています.
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];
}
概要の理解
まず,このコンポーネントの template はフラグメントなので,2 つのテンプレートが生成され,その結果の node 2 つを返していことが分かります.
const t0 = _template("<p></p>");
const t1 = _template("<div><div><span></span></div></div>");
function _sfc_render(_ctx) {
const n0 = t0();
const n4 = t1();
// 省略
return [n0, n4];
}
t0
や t1
を見てもらうとわかる通り,テンプレートからはテキストが取り除かれ,要素のみになっています.
そして,n0
に対しては 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();
// 省略
_renderEffect(() => {
_setText(n0, "count is ", _ctx.count);
// 省略
});
return [n0, n4];
}
ここまでは前回までに見た実装で理解できるはずです.\
問題は
<div>
<div>
{{ "count" }} : <span>{{ count }}</span>
</div>
</div>
部分のコンパイルです.
必要な部分だけ抜き出すと,
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);
});
// 省略
}
で,まずわかることは,count :
の部分は createTextNode
によって生成され,n4
の firstChild
の前に挿入されていいます.
const t1 = _template("<div><div><span></span></div></div>");
const n4 = t1();
const n3 = n4.firstChild;
const n1 = _createTextNode(["count", " : "]);
_prepend(n3, n1);
そして,<span></span>
の部分は setText
で挿入されています.
少しわかりづらいので,各 Node がどれに該当するかだけコメントをつけてみます.
(わかりやすいように要素に id を振りました.)
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];
}
確かにこれで成り立っていそうです.
今回コンパイラの実装で確認したいことは以下です.
- ネストした時に firstChild にアクセスするコードを出力する実装
- createTextNode を出力する実装
- prepend で要素を挿入する出力の実装
コンパイラ (transformer) を読む
さて,また AST から読んでいってもいいのですが,たまには味変で IR
から逆引きしてみましょう.
おそらくそろそろ慣れてきているはずなので,IR
もあらかた当たりをつけることができるはずです.
{
"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]
}
}
かなり長いですが,落ち着いて読めばわかるはずです.
まず,当然ですが IRNode
があります.これが持つ block の children がフラグメントになっている template 部分で,id
がそれぞれ 0
と 4
が振られています.template
プロパティにも template
の 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
}
]
}
}
}
まずこの時点で 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();
は生成できそうです.
なぜ急に id
が 4
に飛んでいるかは,children のなかに潜っていき,内から外に登ってくるからです.transformChildren
の id
が生成されるのは以下のタイミングでした.
23 childContext.reference()
これは,この要素の children のを transformNode した後で行われるので,そこからまた再帰的に入った transformChildren
が先に処理されます.
20 transformNode(childContext)
つまり,id
の生成は末端から親にかけてインクリメントされていきます.
今回はたまたま,t1
が持つ子 Node が #inner
と span
とその中の Text の 3 つがあるのでこれらにそれぞれ 3
, 2
, 1
の id が振られ (0 は t0
から得た Node なので),t1
から得られる Node には 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 }
]
}
]
}
]
}
}
}
operation と effect 部分の IR
も見てみましょう.
{
"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
}
]
}
effect は IRSetText
が 2 つ, operation には IRCreateTextNode
と IRPrependNode
があります.
これだけ生成できていればもう codegen できそうです.
setText については問題ないでしょう.
これまでにみた transformText
の部分を追えば良いです.
IRCreateTextNode
をみてみましょう.
こちらも同じく 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}
この processTextLike
は 2 つめの分岐である,自身が 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) {
最後に IRPrependNode
です.
PREPEND_NODE が登録される部分は以下のところです.
68 } else {
69 context.registerOperation({
70 type: IRNodeTypes.PREPEND_NODE,
71 elements: prevDynamics.map(child => child.id!),
72 parent: context.reference(),
73 })
74 }
これがどこかというと,processDynamicChildren
という関数で,transformChildren
中の処理で,isFragment
が falsy
の場合に呼ばれます.
children から一つづつ child を取り出して,DynamicFlag.INSERT
が立ってる Node を収集します.
今回は,
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}
からも分かる通り,このフラグが立っています.
この辺りの処理で IR
に transform されることがわかりました.
Codegen を読む
IR
さえできれば codegen は何も難しくないのでさらっとみていきます.
新規の部分は主に IRCreateTextNode
と 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}
firstChild
へのアクセスのコード生成は genChildren
で分岐されています.
firstChild がやや特殊で,それ以外の場合は 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
が from
や id
を引き回しながら再帰的に genChildren
を実行しています.
ランタイムを読む
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];
}
のランタイムコードも大したことないのでサクサクいきます.
createTextNode
まずは createTextNode
です.
本当に document.createTextNode
しているだけです.values
として配列か,その getter 関数を受け取っています.
getter の場合には dynamic なものとみなし 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 は本当に ParentNode.prepend
を呼んでいるだけです.
31export function prepend(parent: ParentNode, ...blocks: Block[]): void {
32 parent.prepend(...normalizeBlock(blocks))
33}
かなりサクッとですがここまでで少し複雑なテンプレートがどのように扱われているかわかるようになりました.
これらの children を追従したり,prepend したりという知識はこれからのイベントハンドラをアタッチする時なども同じです.
あとは,単純な構造でひたすらいろんなディレクティブであったり,コンポーネント機能を見ていきましょう!