マスタッシュとステートのバインド
今回の対象コンポーネント
さて,同じ手順で次々といろんなコンポーネントを読み進めてみましょう.
続いては以下のようなコンポーネントを見てください.
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<p>{{ count }}</p>
</template>
上記のコードでは count は変化しないので,あまり実用的なコードではありませんが,ステートの定義とマスタッシュによるバインディングを行ってみました.
コンパイル結果
まずはこの SFC がどのようなコンパイル結果になるかを見てみましょう.
import { ref } from "vue";
const _sfc_main = {
vapor: true,
__name: "App",
setup(__props, { expose: __expose }) {
__expose();
const count = ref(0);
const __returned__ = { count, ref };
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
};
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;
}
import _export_sfc from "/@id/__x00__plugin-vue:export-helper";
export default /*#__PURE__*/ _export_sfc(_sfc_main, [
["render", _sfc_render],
["vapor", true],
["__file", "/path/to/core-vapor/playground/src/App.vue"],
]);
前回のコンポーネントよりも少し複雑になっていますが,基本的な構造は変わりません.
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>
の部分が,
import { ref } from "vue";
const _sfc_main = {
vapor: true,
__name: "App",
setup(__props, { expose: __expose }) {
__expose();
const count = ref(0);
const __returned__ = { count, ref };
Object.defineProperty(__returned__, "__isScriptSetup", {
enumerable: false,
value: true,
});
return __returned__;
},
};
にコンパイルされ,
<template>
<p>{{ count }}</p>
</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;
}
script 部分に関してはこれは vuejs/core-vapor の実装ではなく,元からある compiler-sfc
の実装によるものです.
まぁ,<scirpt setup>
の登場以前から Vue.js を触ってる人からすると,これはなんとなく見慣れた感じがしていると思います.
例の如く compileScript
という関数で実装されています.ここは今回は読み飛ばします.
今回メインで注目したいのは,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;
}
まず,template としては <p>{{ count }}</p>
と書いた部分が <p></p>
に変換されています.
間の {{ count }}
は _renderEffect
と _setText
によって count
の値が更新されるたびに更新されるようになっています.
setText
は名前から予想できる通り,指定した要素にテキストをセットする関数です.renderEffect
とはなんでしょうか...?
こちらは一言で言うと,「update hook 実行付きの watchEffect」です.
Vue.js には watchEffect
という API があります.
https://vuejs.org/api/reactivity-core.html#watcheffect
この関数は,引数に渡したたコールバック関数を初回に実行しつつ,そのコールバックをトラックする関数です.
つまり,初回実行以降,今回で言うと _ctx.count
というリアクティブな変数が更新されるたびに,コールバック関数が再実行されるというわけです.
イメージ的には,
watch(
() => ctx.count,
() => setText(n0, _ctx.count),
{ immediate: true }
);
に近いです.これにより count が更新されるたびに,setText
が実行され,n0
のテキストが更新されるようになります.(画面が更新される)
renderEffect
のもう一つ重要なポイントが.
update hook 実行付きの
の部分です.
Vue.js は画面が更新され前後に beforeUpdate
と updated
というライフサイクルフックを提供しています.
通常の watch はコールバックの実行時にこれらのフックが実行されません.
(画面の更新をハンドリングするものではないので当然です.)
しかし,今回のエフェクトは紛れもなく画面を更新させるためのものです.renderEffect
はこの画面更新の前後に beforeUpdate
と updated
フックを実行するようになっています.
画面をレンダリングするためのエフェクトを作るための関数だということです.
逆に言えば,コンパイラは画面を更新させるようなエフェクトを全て renderEffect
でラップします.
コンパイラの実装を読む
まずは template の AST を出力してみましょう.
{
"type": "Root",
"source": "\n <p>{{ count }}</p>\n",
"children": [
{
"type": "Element",
"tag": "p",
"ns": 0,
"tagType": 0,
"props": [],
"children": [
{
"type": "Interpolation",
"content": {
"type": "SimpleExpression",
"content": "count",
"isStatic": false,
"constType": 0,
"ast": null
}
}
]
}
],
"helpers": {},
"components": [],
"directives": [],
"hoists": [],
"imports": [],
"cached": [],
"temps": 0
}
Template AST の概要で一通りの Node はみているので,もう意味は分かるかと思います.
そして,Parser の実装もみているのでどのようにこのオブジェクトを得られるかも皆さんはすでに知っているはずです.
transformer を読む
次は,これをどのように transform していくのか,という実装を見ていきましょう.
おそらくこれからもこういった流れ (AST, Parse はさらっと流して transformer をしっかり読む) が多くなると思います.
例の如く,transform
->transformNode
に入ると,NodeTransformer が実行されます.transformElement
(onExit) -> tarnsformChildren
と入っていき,transfoemText
に入ってきます.
ここまではいつも通りで,ここからが今回のポイントです.
22export const transformText: NodeTransform = (node, context) => {
今回はこのチェックを通る時,
89function isAllTextLike(children: TemplateChildNode[]): children is TextLike[] {
90 return (
91 !!children.length &&
92 children.every(isTextLike) &&
93 // at least one an interpolation
94 children.some(n => n.type === NodeTypes.INTERPOLATION)
95 )
96}
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 )
processTextLikeContainer
を見てみましょう.
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}
どうやらここで registerEffect
という関数を呼び出しています.
そして,正しく type: IRNodeTypes.SET_TEXT
になっています.
リテラルも取得し,全て null じゃなかった場合にはそのまま連結して context.childrenTemplate
に追加し終了します.
(つまり template
の引数に落ちる)
逆に,そうじゃない場合は context.childrenTemplate
が空っぽなままなので,この部分は template
の引数には乗りません.
(今回の場合,最終的な template は "<p></p>"
になる)
そうじゃない場合は registerEffect
です.context.reference
を実行し,この Node を変数に保持することをマークしつつ id を取得します.
registerEffect
registerEffect
という関数の中身を少しみてみましょう.
137 registerEffect(
138 expressions: SimpleExpressionNode[],
139 ...operations: OperationNode[]
140 ): void {
引数として,expressions
と operations
を受け取ります.
expression
は AST の SimpleExpression
です. (e.g. count
, obj.prop
など)
operations
は新概念です.
これは IR
の一種で,OperationNode
というものです.
211export type OperationNode =
212 | SetPropIRNode
213 | SetDynamicPropsIRNode
214 | SetTextIRNode
215 | SetEventIRNode
216 | SetDynamicEventsIRNode
217 | SetHtmlIRNode
218 | SetTemplateRefIRNode
219 | SetModelValueIRNode
220 | CreateTextNodeIRNode
221 | InsertNodeIRNode
222 | PrependNodeIRNode
223 | WithDirectiveIRNode
224 | IfIRNode
225 | ForIRNode
226 | CreateComponentIRNode
227 | DeclareOldRefIRNode
228 | SlotOutletIRNode
この定義を見れば想像はつくと思いますが,「操作」を表す Node です.
例えば,SetTextIRNode
は「テキストをセットする」という操作です.
他にも,イベントをセットする SetEventIRNode
やコンポーネントを生成する CreateComponentIRNode
などがあります.
今回は SetTextIRNode
が使われているので少しみてみましょう.
108export interface SetTextIRNode extends BaseIRNode {
109 type: IRNodeTypes.SET_TEXT
110 element: number
111 values: SimpleExpressionNode[]
112}
SetTextIRNode
は element の id (number) と values (SimpleExpression[]) を持ちます.
例えば,id が 0 で value が count
を表す SimpleExpression だとすると,
setText(n0, count);
というようなコードの IR
を表現することになります.
registerEffect
の続きに戻ると,
151 this.block.effect.push({
152 expressions,
153 operations,
154 })
入ってきた expressions
と operations
を block.effect
に push しています.
block.effect
は
51 effect: IREffect[]
です.
これで概ねマスタッシュの IR
の生成が完了しました.
あとはこれを元に codegen していくだけです.
Codegen を読む
まぁ,予想通り特に難しいところはありません.block
が持っている effect
を type
によって分岐して処理していくだけです.
おそらく何も説明なしで読めると思います.
36export function genBlockContent(
37 block: BlockIRNode,
38 context: CodegenContext,
39 root?: boolean,
40 customReturns?: (returns: CodeFragment[]) => CodeFragment[],
41): CodeFragment[] {
56 push(...genEffects(effect, context))
75export function genEffects(
76 effects: IREffect[],
77 context: CodegenContext,
78): CodeFragment[] {
79 const [frag, push] = buildCodeFragment()
80 for (const effect of effects) {
81 push(...genEffect(effect, context))
86export function genEffect(
87 { operations }: IREffect,
88 context: CodegenContext,
89): CodeFragment[] {
90 const { vaporHelper } = context
91 const [frag, push] = buildCodeFragment(
92 NEWLINE,
93 `${vaporHelper('renderEffect')}(() => `,
94 )
95
96 const [operationsExps, pushOps] = buildCodeFragment()
97 operations.forEach(op => pushOps(...genOperation(op, context)))
98
99 const newlineCount = operationsExps.filter(frag => frag === NEWLINE).length
100 if (newlineCount > 1) {
101 push('{', INDENT_START, ...operationsExps, INDENT_END, NEWLINE, '})')
102 } else {
103 push(...operationsExps.filter(frag => frag !== NEWLINE), ')')
104 }
105
106 return frag
107}
33export function genOperation(
34 oper: OperationNode,
35 context: CodegenContext,
36): CodeFragment[] {
42 case IRNodeTypes.SET_TEXT:
43 return genSetText(oper, context)
12export function genSetText(
13 oper: SetTextIRNode,
14 context: CodegenContext,
15): CodeFragment[] {
16 const { vaporHelper } = context
17 const { element, values } = oper
18 return [
19 NEWLINE,
20 ...genCall(
21 vaporHelper('setText'),
22 `n${element}`,
23 ...values.map(value => genExpression(value, context)),
24 ),
25 ]
26}
なんとあっさり!コンパイラ完全制覇です!
ランタイムを読む
さて,コンパイル結果の
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;
}
のランタイム(実際の動作)部分を読んでいきましょう.
application のエントリで component のインスタンスが作られ,コンポーネントの render
関数が呼ばれてその結果の node が container に入っていくのは今までと同じです.
実際に render
が実行された時に何が起こるかを見ていきましょう.
まずは setText
です.
この辺りのオペレーションは概ね packages/runtime-vapor/src/dom に実装されています.
setText
の実装は以下です.
188export function setText(el: Node, ...values: any[]): void {
189 const text = values.map(v => toDisplayString(v)).join('')
190 const oldVal = recordPropMetadata(el, 'textContent', text)
191 if (text !== oldVal) {
192 el.textContent = text
193 }
194}
本当に単純なことしかしていません.ただの DOM 操作です. values
を join
して el
の textContent
に突っ込みます.
あとは renderEffect
の実装を見てこのページは終わりにしましょう.
改めて renderEffect
は「update hook 実行付きの watchEffect」です.
実装は packages/runtime-vapor/src/renderEffect.ts にあります.
現在の instance や effectScope を設定しつつ,コールバックをラップして,
19 const instance = getCurrentInstance()
20 const scope = getCurrentScope()
21
22 if (scope) {
23 const baseCb = cb
24 cb = () => scope.run(baseCb)
25 }
26
27 if (instance) {
28 const baseCb = cb
29 cb = () => {
30 const reset = setCurrentInstance(instance)
31 baseCb()
32 reset()
33 }
34 job.id = instance.uid
35 }
ReactiveEffect
を生成します.
37 const effect = new ReactiveEffect(() =>
38 callWithAsyncErrorHandling(cb, instance, VaporErrorCodes.RENDER_FUNCTION),
39 )
effect.scheduler
(effect.run 経由ではなく,trigger 等で呼ばれる動作) には job
という関数 (後述) を設定しています.
41 effect.scheduler = () => queueJob(job)
以下が初回実行になります.
50 effect.run()
job
部分です.
52 function job() {
effect
の実行の前にライフサイクルフック (beforeUpdate) を実行します.
62 const { bu, u, scope } = instance
63 const { dirs } = scope
64 // beforeUpdate hook
65 if (bu) {
66 invokeArrayFns(bu)
67 }
68 if (dirs) {
69 invokeDirectiveHook(instance, 'beforeUpdate', scope)
70 }
そして effect
の実行を行い.
72 effect.run()
最後にライフサイクルフック (updated) を実行します.
実際にはスケジューラのキューに積んでいるだけです.
(スケジューラがいい感じに重複排除等を行って然るべきところで実行されます)
74 queuePostFlushCb(() => {
75 instance.isUpdating = false
76 const reset = setCurrentInstance(instance)
77 if (dirs) {
78 invokeDirectiveHook(instance, 'updated', scope)
79 }
80 // updated hook
81 if (u) {
82 queuePostFlushCb(u)
83 }
84 reset()
85 })
そろそろスケジューラ周りの実装がよく出てくるよになったので,次のページではスケジューラの実装について少し見てみましょう!