Transformer の概要
実装箇所
続いて,AST
を IR
に変換するための 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 種類のインターフェイスがあります.NodeTransform
と DirectiveTransform
です.
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 つのいずれかになります.
サクッとそれぞれがどっちなのかをまとめておくと,
- NodeTransform
- DirectiveTransform
といった感じで.名前から想像できる通りだと思います.
これらの 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 {
今回は,今読んでいる,小さいコンポーネント
<template>
<p>Hello, Vapor!</p>
</template>
から得られた AST を IR に transform する処理を追っていきます.
まず,得られた AST は以下のようなものになります.
{
"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
今見ている node
の type
が ELEMENT
の場合で,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 を見てわかる通り,
{
"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
やっていることは単純で,入ってきた node
の children
に対して一つづ順に 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>"
という文字列を作ることができました.
まだまだ終わらない
果たして,文字列を生成することができたのはいいですが,実際には
const t0 = _template("<p>Hello, Vapor!</p>");
function _sfc_render(_ctx) {
const n0 = t0();
return n0;
}
のようなコードを生成しなくてはなりません.
このためにはまだ情報が足りません.
このテンプレートを t0
する実装や,その結果を n0
とし,render の return にする実装はまだみていません.
それがどこで行われているかは次のページで見てみましょう.