Skip to content

Transformer の概要

実装箇所

続いて,ASTIR に変換するための Transformer についてみていきます.

compiler vapor transformer

コンパイラの概要の際にも話した通り,transformer というコンセプト自体は vuejs/core の時から,compiler-core に存在していました. その実装はこのあたりにあります.

Vapor Mode には関係ないので今回は読み飛ばしますが,Vapor Mode の transformer もこの元々あった transformer を参考に設計されています.(使われてはいません)
今回読んでいく Vapor Mode の transformer はこのあたりに実装があります.

transform.ts に実装された transform という関数をコンパイラで呼び出しています.

209// AST -> IR
210export function transform(
211  node: RootNode,
212  options: TransformOptions = {},
213): RootIRNode {

呼び出し (compile: parse -> transform -> generate):

36// code/AST -> IR (transform) -> JS (generate)
37export function compile(
38  source: string | RootNode,
39  options: CompilerOptions = {},
40): VaporCodegenResult {
62  const ast = isString(source) ? parse(source, resolvedOptions) : source
76  const ir = transform(
77    ast,
78    extend({}, resolvedOptions, {
79      nodeTransforms: [
80        ...nodeTransforms,
81        ...(options.nodeTransforms || []), // user transforms
82      ],
83      directiveTransforms: extend(
84        {},
85        directiveTransforms,
86        options.directiveTransforms || {}, // user transforms
87      ),
88    }),
89  )
91  return generate(ir, resolvedOptions)

Transformer の設計

Transformer には 2 種類のインターフェイスがあります.
NodeTransformDirectiveTransform です.

31export type NodeTransform = (
32  node: RootNode | TemplateChildNode,
33  context: TransformContext<RootNode | TemplateChildNode>,
34) => void | (() => void) | (() => void)[]
36export type DirectiveTransform = (
37  dir: VaporDirectiveNode,
38  node: ElementNode,
39  context: TransformContext<ElementNode>,
40) => DirectiveTransformResult | void

/transforms/ には様々 transformer が実装されていますが,これらはこの 2 つのいずれかになります.

サクッとそれぞれがどっちなのかをまとめておくと,

といった感じで.名前から想像できる通りだと思います.
これらの transformer によって AST を IR に変換していきます.

76  const ir = transform(
77    ast,
78    extend({}, resolvedOptions, {
79      nodeTransforms: [
80        ...nodeTransforms,
81        ...(options.nodeTransforms || []), // user transforms
82      ],
83      directiveTransforms: extend(
84        {},
85        directiveTransforms,
86        options.directiveTransforms || {}, // user transforms
87      ),
88    }),
89  )

からもわかる通り,これらの transformer を transform という関数に対してオプションとして渡しています.

nodeTransforms, directiveTransforms は以下から来ています.

63  const [nodeTransforms, directiveTransforms] =
64    getBaseTransformPreset(prefixIdentifiers)
100export function getBaseTransformPreset(
101  prefixIdentifiers?: boolean,
102): TransformPreset {
103  return [
104    [
105      transformVOnce,
106      transformVIf,
107      transformVFor,
108      transformSlotOutlet,
109      transformTemplateRef,
110      transformText,
111      transformElement,
112      transformVSlot,
113      transformComment,
114      transformChildren,
115    ],
116    {
117      bind: transformVBind,
118      on: transformVOn,
119      html: transformVHtml,
120      text: transformVText,
121      show: transformVShow,
122      model: transformVModel,
123    },
124  ]
125}
16import { transformChildren } from './transforms/transformChildren'
17import { transformVOnce } from './transforms/vOnce'
18import { transformElement } from './transforms/transformElement'
19import { transformVHtml } from './transforms/vHtml'
20import { transformVText } from './transforms/vText'
21import { transformVBind } from './transforms/vBind'
22import { transformVOn } from './transforms/vOn'
23import { transformVShow } from './transforms/vShow'
24import { transformTemplateRef } from './transforms/transformTemplateRef'
25import { transformText } from './transforms/transformText'
26import { transformVModel } from './transforms/vModel'
27import { transformVIf } from './transforms/vIf'
28import { transformVFor } from './transforms/vFor'
29import { transformComment } from './transforms/transformComment'
30import { transformSlotOutlet } from './transforms/transformSlotOutlet'
31import { transformVSlot } from './transforms/vSlot'

transform 関数を読む

早速 transform 関数を読んでみましょう.

210export function transform(
211  node: RootNode,
212  options: TransformOptions = {},
213): RootIRNode {
214  const ir: RootIRNode = {
215    type: IRNodeTypes.ROOT,
216    node,
217    source: node.source,
218    template: [],
219    component: new Set(),
220    directive: new Set(),
221    block: newBlock(node),
222  }
223
224  const context = new TransformContext(ir, node, options)
225
226  transformNode(context)
227
228  return ir
229}

transform 関数は TransformContext というオブジェクトを 1 つ持ちます.
ざっくり,Transform に必要なオプションや,状態を持つためのオブジェクトです.

62export class TransformContext<T extends AllNode = AllNode> {

この,context にある実装は実際の transform 処理を追いながら随時読んでいきましょう.

とりあえず,この context を transformNode という関数に渡して transform 処理が始まります.

224  const context = new TransformContext(ir, node, options)
225
226  transformNode(context)
231export function transformNode(
232  context: TransformContext<RootNode | TemplateChildNode>,
233): void {

今回は,今読んでいる,小さいコンポーネント

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

から得られた AST を IR に transform する処理を追っていきます.

まず,得られた AST は以下のようなものになります.

json
{
  "type": "RootNode",
  "source": "\n  <p>Hello, Vapor!</p>\n",
  "children": [
    {
      "type": "ElementNode",
      "tag": "p",
      "ns": 0,
      "tagType": "Element",
      "props": [],
      "children": [
        {
          "type": "TextNode",
          "content": "Hello, Vapor!"
        }
      ]
    }
  ],
  "helpers": {},
  "components": [],
  "directives": [],
  "hoists": [],
  "imports": [],
  "cached": [],
  "temps": 0
}

まず,この Node が transformNode に入っていき,transformNode は option として渡された nodeTransforms を一つづつ順に実行していきます.

237  const { nodeTransforms } = context.options
238  const exitFns = []
239  for (const nodeTransform of nodeTransforms) {
240    const onExit = nodeTransform(node, context)

設計として,transform を適用した後に最後に実行するものを onExit として受けるようになっています.
これらは後で実行するように保存しておいて,

241    if (onExit) {
242      if (isArray(onExit)) {
243        exitFns.push(...onExit)
244      } else {
245        exitFns.push(onExit)
246      }
247    }

transformNode の最後で実行します.

259  let i = exitFns.length
260  while (i--) {
261    exitFns[i]()
262  }

早速 nodeTransforms の実行を見ていきましょう.
順番は,

105      transformVOnce,
106      transformVIf,
107      transformVFor,
108      transformSlotOutlet,
109      transformTemplateRef,
110      transformText,
111      transformElement,
112      transformVSlot,
113      transformComment,
114      transformChildren,

の通りです.今回はまだディレクティブやスロットは使っていないので,transformText -> transformElement -> transformChildren の順に読んでいこうと思います.

transformText

実装はここにあります.

packages/compiler-vapor/src/transforms/transformText.ts

今見ている nodetypeELEMENT の場合で,children の node が全て text-like で,interpolation を含む場合にはその node を「text のコンテナ」として扱い処理 (processTextLikeContainer) します.
text-like というのは text または interpolation です.

63function processTextLikeContainer(
64  children: TextLike[],
65  context: TransformContext<ElementNode>,
66) {
67  const values = children.map(child => createTextLikeExpression(child, context))
68  const literals = values.map(getLiteralExpressionValue)
69  if (literals.every(l => l != null)) {
70    context.childrenTemplate = literals.map(l => String(l))
71  } else {
72    context.registerEffect(values, {
73      type: IRNodeTypes.SET_TEXT,
74      element: context.reference(),
75      values,
76    })
77  }
78}

今回は,AST を見てわかる通り,

json
{
  "type": "ElementNode",
  "tag": "p",
  "ns": 0,
  "tagType": "Element",
  "props": [],
  "children": [
    {
      "type": "TextNode",
      "content": "Hello, Vapor!"
    }
  ]
}

今回は interpolation を含まないのでこの分岐に入りません.

少し順番は前後しますが,今回は次々 Node が読み進められ,TextNode に入った時に下の下の以下の分岐を通ります.

40  } else if (node.type === NodeTypes.TEXT) {
41    context.template += node.content
42  }

context の template というプロパティに text node の content を追加して終了です.
template は "Hello, Vapor!" になります.

transformElement

実装はここにあります.

packages/compiler-vapor/src/transforms/transformElement.ts

まず前提として,この transform は全体として onExit のライフサイクルに乗っています.
関数を return している事に注目してください.

43  return function postTransformElement() {

今回は Component ではないので,transformNativeElement が実行されることになります (今 p タグを読んでいると仮定してください).

55    const isComponent = tagType === ElementTypes.COMPONENT
62    ;(isComponent ? transformComponentElement : transformNativeElement)(
63      tag,
64      propsResult,
65      context as TransformContext<ElementNode>,
66    )
130function transformNativeElement(
131  tag: string,
132  propsResult: PropsResult,
133  context: TransformContext<ElementNode>,
134) {

transformNativeElement では,template 関数に引数として渡すための文字列を生成します.

まずは AST から tag 名を取り出し,< にくっつけます.

137  let template = ''
138
139  template += `<${tag}`

props がある場合はそれも生成しますが.今回は props がないので一旦スキップします.

あとは,context に保持してある childrenTemplate というものを差し込みつつ,閉じタグを生成したら終了です.

165  template += `>` + context.childrenTemplate.join('')
166  // TODO remove unnecessary close tag, e.g. if it's the last element of the template
167  if (!isVoidTag(tag)) {
168    template += `</${tag}>`
169  }

childrenTemplate がどこで作られているかというと,transformChildren です.

transform の実行順的には, transformText -> transformElement -> transformChildren なのですが,今見た transformElement の処理は onExit で実行され,先に transformChildren が実行される事になるため,すでに childrenTemplate は生成されています.

それでは実際に childrenTemplate を作っているところを見てみましょう.

transformChildren

実装はここにあります.

packages/compiler-vapor/src/transforms/transformChildren.ts

やっていることは単純で,入ってきた nodechildren に対して一つづ順に transformNode を実行していきます.

18  for (const [i, child] of node.children.entries()) {
19    const childContext = context.create(child, i)
20    transformNode(childContext)

ここで面白いのが, child node に入ったらまず child node 専用の context (childContext) を新たに生成しているところです.
そして,transformNode が済んだら,その childContext に保持されている template を取り出して,親の context に push します.
(push はただの Array.prototype.push です)

32    } else {
33      context.childrenTemplate.push(childContext.template)
34    }

context.template"<p>Hello, Vapor!</p>" という文字列を作ることができました.

まだまだ終わらない

果たして,文字列を生成することができたのはいいですが,実際には

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

のようなコードを生成しなくてはなりません.
このためにはまだ情報が足りません.
このテンプレートを t0 する実装や,その結果を n0 とし,render の return にする実装はまだみていません.
それがどこで行われているかは次のページで見てみましょう.