Transformer の概要 2
少し長くなったので分割しましたが続きです.
概要,といいつつ気付けば具体的な実装の詳細を読んでしまっていますが,まぁ,自然な導入としては良いのではないでしょうか (笑)
前ページまでで template
関数に引数として渡すテンプレートの文字列を生成する実装を見てきました.
ここでは Block
の index 管理等を見ていきます.
registerTemplate
TransformContext
に registerTemplate
という関数があります.
131 registerTemplate(): number {
132 if (!this.template) return -1
133 const id = this.pushTemplate(this.template)
134 return (this.dynamic.template = id)
135 }
この registerTemplate
では pushTemplate
という関数を呼んでいます.
123 pushTemplate(content: string): number {
124 const existing = this.ir.template.findIndex(
125 template => template === content,
126 )
127 if (existing !== -1) return existing
128 this.ir.template.push(content)
129 return this.ir.template.length - 1
130 }
template (文字列) は this.ir.template
(配列) に登録されていきます.
this.ir
は RootIRNode
です.
87 constructor(
88 public ir: RootIRNode,
つまりここです.
56export interface RootIRNode {
57 type: IRNodeTypes.ROOT
58 node: RootNode
59 source: string
60 template: string[]
そして,この registerTemplate
が呼び出されるのは 3 箇所です.
transformNode
の最後 (root の時のみ)
264 if (context.node.type === NodeTypes.ROOT) {
265 context.registerTemplate()
266 }
transformChildren
で children を処理したあと (Fragment の時のみ)
22 if (isFragment) {
23 childContext.reference()
24 childContext.registerTemplate()
context.enterBlock
が呼び出された時 (onExit
)
104 return () => {
105 // exit
106 this.registerTemplate()
107 this.block = block
108 this.template = template
109 this.dynamic = dynamic
110 this.childrenTemplate = childrenTemplate
111 this.slots = slots
112 isVFor && this.inVFor--
113 }
context.enterBlock
は transformVFor
や transformVIf
で Block
に入るときに呼び出す関数です.
これはまた v-for
や v-if
のコンパイルの実装を見る時に見るとして,一旦は 1 と 2 だけ把握しておけば良いでしょう.
1 に関しては特に何もないと思います.
今回読んでいる小さいコンポーネントはここで template が登録されます.
つまり,この時点で今,this.ir.template
は,["<p>Hello, Vapor!</p>"]
という状態になっています.
これがあれば template の index がわかるので,
const t0 = template("<p>Hello, Vapor!</p>");
というコードは生成することができそうです.(ここはまた codegen の時に実際に見てみましょう.)
2 の Fragment の場合というのは,
<template>
<p>Hello, Vapor 1</p>
<p>Hello, Vapor 2</p>
<p>Hello, Vapor 3</p>
</template>
のような template を書いた場合です.この場合は 2 のタイミングで 3 つの template が登録されます.
// this.ir.template
["<p>Hello, Vapor 1</p>", "<p>Hello, Vapor 2</p>", "<p>Hello, Vapor 3</p>"];
const t0 = template("<p>Hello, Vapor 1</p>");
const t1 = template("<p>Hello, Vapor 2</p>");
const t2 = template("<p>Hello, Vapor 3</p>");
render 関数の戻り値
<template>
<p>Hello, Vapor!</p>
</template>
に話を戻して,これから得られた IR
を再度見返してみます.
(不要な部分は省略しています)
{
"type": "RootIRNode",
"template": ["<p>Hello, Vapor!</p>"],
"block": {
"type": "BlockIRNode",
"returns": [0]
}
}
よく見ると,"returns": [0]
というなんとも怪しいものがあります.
この情報があれば,0 番目の node が render 関数の戻り値となりそうなことが分かります.
これは,transformChildren
で行われています.
26 if (
27 !(childContext.dynamic.flags & DynamicFlag.NON_TEMPLATE) ||
28 childContext.dynamic.flags & DynamicFlag.INSERT
29 ) {
30 context.block.returns.push(childContext.dynamic.id!)
31 }
ある条件下の時に,その node の id
を block.returns
に push しています.
この id は pushTemplate
した際に length から算出されています.
123 pushTemplate(content: string): number {
124 const existing = this.ir.template.findIndex(
125 template => template === content,
126 )
127 if (existing !== -1) return existing
128 this.ir.template.push(content)
129 return this.ir.template.length - 1
130 }
131 registerTemplate(): number {
132 if (!this.template) return -1
133 const id = this.pushTemplate(this.template)
134 return (this.dynamic.template = id)
135 }
そして,
ある条件下の時
これがどういう場合かというと,まず 1 つ目の条件は isFragment
が true
の時です.
これは transformChildren
を実行している node
が Root
, Element
, Template
, Component
のいずれかの場合です.
10 const isFragment =
11 node.type === NodeTypes.ROOT ||
12 (node.type === NodeTypes.ELEMENT &&
13 (node.tagType === ElementTypes.TEMPLATE ||
14 node.tagType === ElementTypes.COMPONENT))
そして 2 つめの条件は, dynamic.flags
が NON_TEMPLATE
では ない もしくは,INSERT
である場合です.
(※ ぱっと見分かりづらいですが,ビットマスクなので各フラグは排他的なものではありません)
27 !(childContext.dynamic.flags & DynamicFlag.NON_TEMPLATE) ||
28 childContext.dynamic.flags & DynamicFlag.INSERT
この 2 つの条件に一致した場合に block.returns
に id
が push されます.
1 つ目の条件はまぁ良いと思います.
2 つめの条件の,dynamic.flags
についてです.
dynamic.flags
dynamic
は TransformContext
のプロパティです.
74 dynamic: IRDynamicInfo = this.ir.block.dynamic
246export interface IRDynamicInfo {
247 id?: number
248 flags: DynamicFlag
249 anchor?: number
250 children: IRDynamicInfo[]
251 template?: number
252}
context.ir
がこの情報を持っているので,その参照を TransformContext
に保持しています.
特に,今回はこの IRDynamicInfo
が持つ,DynamicFlag
という情報が重要なのでそこを重点的にみていきます.
DynamicFlag
は node がどのような性質を持っているかを表すフラグです.
性質は各自コメントアウトにある通りです.
230export enum DynamicFlag {
231 NONE = 0,
232 /**
233 * This node is referenced and needs to be saved as a variable.
234 */
235 REFERENCED = 1,
236 /**
237 * This node is not generated from template, but is generated dynamically.
238 */
239 NON_TEMPLATE = 1 << 1,
240 /**
241 * This node needs to be inserted back into the template.
242 */
243 INSERT = 1 << 2,
244}
ビットマスクで表現されているため,各性質は共存しうります.
それぞれ,どういう時にそのフラグがマークされるかどうかみてみましょう.
DynamicFlag.REFERENCED
This node is referenced and needs to be saved as a variable.
という記載があります.
DynamicFlag.REFERENCED
が設定されるタイミングは 2 箇所です.
context.reference
が呼ばれた時.
117 reference(): number {
118 if (this.dynamic.id !== undefined) return this.dynamic.id
119 this.dynamic.flags |= DynamicFlag.REFERENCED
120 return (this.dynamic.id = this.increaseId())
121 }
newDynamic
によってIRDynamicInfo
を生成した時 (デフォルト値として)
20export const newDynamic = (): IRDynamicInfo => ({
21 flags: DynamicFlag.REFERENCED,
22 children: [],
23})
まず 1 のケースですが,context.reference
はかなり色々なところで呼ばれています.
例えば,先ほど見ていた transformChildren
の isFragment
が true
の時の条件分岐の中で呼ばれています.
22 if (isFragment) {
23 childContext.reference()
24 childContext.registerTemplate()
そして,このフラグがなんのために使われているのかというと,コードを生成する際に id
を生成するためです.
これはまた後ほど codegen の実装を見ていく際に詳しく見ますが,このフラグが立っている Node は id
を生成し,変数に保持します.
needs to be saved as a variable.
の通りです.
transformChildren
の isFragment
でこのフラグが立たせているのもこれでよくわかると思います.
こうすることで,
const n0 = t0();
const n1 = t1();
const n2 = t2();
のように n${id}
という変数に保持するコードを出力することができます.
逆に,変数に保持する必要のない node はこのフラグが立ません.
今回は,
const t0 = template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
const n0 = t0(); // ここ
return n0;
}
のように n0
という変数に保持する必要があるので,このフラグが立っているということです.
DynamicFlag.NON_TEMPLATE
続いて DynamicFlag.NON_TEMPLATE
です.
このフラグが立っているかどうかはかなり重要で,立っていなければ block.returns
に id
が push されていく事になります.
26 if (
27 !(childContext.dynamic.flags & DynamicFlag.NON_TEMPLATE) ||
28 childContext.dynamic.flags & DynamicFlag.INSERT
29 ) {
30 context.block.returns.push(childContext.dynamic.id!)
31 }
This node is not generated from template, but is generated dynamically.
とあるように,どうやら template から生成された node ではないものにこのフラグが立つようです.
例えば,transformComponentElement
や transformSlotOutlet
, transformVFor
などでこのフラグが立つようです.
96 context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
31 context.dynamic.flags |= DynamicFlag.INSERT | DynamicFlag.NON_TEMPLATE
50 context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
40 context.dynamic.flags |= DynamicFlag.NON_TEMPLATE
115 context.dynamic.flags |= DynamicFlag.NON_TEMPLATE
少し飛ばしてこのフラグと併用して重要なポイントになるのが DynamicFlag.INSERT
です.
DynamicFlag.INSERT
returns
に id を push するかどうかはまず,DynamicFlag.NON_TEMPLATE
が立っていないかどうかを見ます.
もし立っていなければこの時点で returns
に push されます.
立っていた場合は,DynamicFlag.INSERT
が立っているかどうかを見ます.
96 context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
31 context.dynamic.flags |= DynamicFlag.INSERT | DynamicFlag.NON_TEMPLATE
50 context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
を見るとわかる通り,Component
, SlotOutlet
, v-for
は初めからこのフラグが立っています.
しかし,
40 context.dynamic.flags |= DynamicFlag.NON_TEMPLATE
115 context.dynamic.flags |= DynamicFlag.NON_TEMPLATE
はこの時点では立っていません.
if に関して言えば,v-if
の場合 (v-else-if
, v-else
ではない場合) にこのフラグを立てます.
41 if (dir.name === 'if') {
42 const id = context.reference()
43 context.dynamic.flags |= DynamicFlag.INSERT
そして,<template #foo>
のような,挿入されたスロットに場合にはこのフラグが立つことはありません.
110function transformTemplateSlot(
111 node: ElementNode,
112 dir: VaporDirectiveNode,
113 context: TransformContext<ElementNode>,
114) {
115 context.dynamic.flags |= DynamicFlag.NON_TEMPLATE
このようにして,block.returns
に何を push して,何を push しないかを選択しています.
今回の小さなコンポーネントの場合には,DynamicFlag.NON_TEMPLATE
というフラグは立たないので,block.returns
には id
が push されます. これで,codegen に必要そうな IR
を全て生成 (transform) することができました!
次は,codegen の実装を見ていきましょう!