Skip to content

Transformer の概要 2

少し長くなったので分割しましたが続きです.
概要,といいつつ気付けば具体的な実装の詳細を読んでしまっていますが,まぁ,自然な導入としては良いのではないでしょうか (笑)

前ページまでで template 関数に引数として渡すテンプレートの文字列を生成する実装を見てきました.
ここでは Block の index 管理等を見ていきます.

registerTemplate

TransformContextregisterTemplate という関数があります.

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.irRootIRNode です.

87  constructor(
88    public ir: RootIRNode,

つまりここです.

56export interface RootIRNode {
57  type: IRNodeTypes.ROOT
58  node: RootNode
59  source: string
60  template: string[]

そして,この registerTemplate が呼び出されるのは 3 箇所です.

  1. transformNode の最後 (root の時のみ)
264  if (context.node.type === NodeTypes.ROOT) {
265    context.registerTemplate()
266  }
  1. transformChildren で children を処理したあと (Fragment の時のみ)
22    if (isFragment) {
23      childContext.reference()
24      childContext.registerTemplate()
  1. 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.enterBlocktransformVFortransformVIfBlock に入るときに呼び出す関数です.
これはまた v-forv-if のコンパイルの実装を見る時に見るとして,一旦は 1 と 2 だけ把握しておけば良いでしょう.

1 に関しては特に何もないと思います.
今回読んでいる小さいコンポーネントはここで template が登録されます.
つまり,この時点で今,this.ir.template は,["<p>Hello, Vapor!</p>"] という状態になっています.

これがあれば template の index がわかるので,

js
const t0 = template("<p>Hello, Vapor!</p>");

というコードは生成することができそうです.(ここはまた codegen の時に実際に見てみましょう.)

2 の Fragment の場合というのは,

vue
<template>
  <p>Hello, Vapor 1</p>
  <p>Hello, Vapor 2</p>
  <p>Hello, Vapor 3</p>
</template>

のような template を書いた場合です.この場合は 2 のタイミングで 3 つの template が登録されます.

js
// this.ir.template
["<p>Hello, Vapor 1</p>", "<p>Hello, Vapor 2</p>", "<p>Hello, Vapor 3</p>"];
js
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 関数の戻り値

vue
<template>
  <p>Hello, Vapor!</p>
</template>

に話を戻して,これから得られた IR を再度見返してみます.
(不要な部分は省略しています)

json
{
  "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 の idblock.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 つ目の条件は isFragmenttrue の時です.
これは transformChildren を実行している nodeRoot, 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.flagsNON_TEMPLATE では ない もしくは,INSERT である場合です.
(※ ぱっと見分かりづらいですが,ビットマスクなので各フラグは排他的なものではありません)

27        !(childContext.dynamic.flags & DynamicFlag.NON_TEMPLATE) ||
28        childContext.dynamic.flags & DynamicFlag.INSERT

この 2 つの条件に一致した場合に block.returnsid が push されます.
1 つ目の条件はまぁ良いと思います.
2 つめの条件の,dynamic.flags についてです.

dynamic.flags

dynamicTransformContext のプロパティです.

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 箇所です.

  1. 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  }
  1. newDynamic によって IRDynamicInfo を生成した時 (デフォルト値として)
20export const newDynamic = (): IRDynamicInfo => ({
21  flags: DynamicFlag.REFERENCED,
22  children: [],
23})

まず 1 のケースですが,context.reference はかなり色々なところで呼ばれています.
例えば,先ほど見ていた transformChildrenisFragmenttrue の時の条件分岐の中で呼ばれています.

22    if (isFragment) {
23      childContext.reference()
24      childContext.registerTemplate()

そして,このフラグがなんのために使われているのかというと,コードを生成する際に id を生成するためです.
これはまた後ほど codegen の実装を見ていく際に詳しく見ますが,このフラグが立っている Node は id を生成し,変数に保持します.

needs to be saved as a variable.

の通りです.

transformChildrenisFragment でこのフラグが立たせているのもこれでよくわかると思います.
こうすることで,

js
const n0 = t0();
const n1 = t1();
const n2 = t2();

のように n${id} という変数に保持するコードを出力することができます.
逆に,変数に保持する必要のない node はこのフラグが立ません.

今回は,

js
const t0 = template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
  const n0 = t0(); // ここ
  return n0;
}

のように n0 という変数に保持する必要があるので,このフラグが立っているということです.

DynamicFlag.NON_TEMPLATE

続いて DynamicFlag.NON_TEMPLATE です.
このフラグが立っているかどうかはかなり重要で,立っていなければ block.returnsid が 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 ではないものにこのフラグが立つようです.

例えば,transformComponentElementtransformSlotOutlet, 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 の実装を見ていきましょう!