v-if ディレクティブ
以下のようなコンポーネントを考えます.
<script setup>
import { ref } from "vue";
const ok = ref(true);
</script>
<template>
<div v-if="ok">Hello, v-if!</div>
</template>コンパイル結果と概要
コンパイル結果は以下のようになります.
import {
createIf as _createIf,
template as _template,
} from "vue/vapor";
const t0 = _template("<div>Hello, v-if!</div>");
function _sfc_render(_ctx) {
const n0 = _createIf(
() => _ctx.ok,
() => {
const n2 = t0();
return n2;
}
);
return n0;
}v-show とは異なり,v-if は条件に応じて DOM 要素を生成・破棄します.
そのため,createIf というヘルパー関数を使って,条件分岐を実現しています.
createIf は第一引数に条件,第二引数に条件が真の場合に実行するブロック (positive),第三引数に条件が偽の場合に実行するブロック (negative) を取ります.
v-if + v-else
以下のようなコンポーネントを考えます.
<template>
<div v-if="ok">YES</div>
<p v-else>NO</p>
</template>コンパイル結果は以下のようになります.
import { createIf as _createIf, template as _template } from "vue/vapor";
const t0 = _template("<div>YES</div>");
const t1 = _template("<p>NO</p>");
function _sfc_render(_ctx) {
const n0 = _createIf(
() => _ctx.ok,
() => {
const n2 = t0();
return n2;
},
() => {
const n4 = t1();
return n4;
}
);
return n0;
}第三引数に v-else のブロックが渡されています.
v-if + v-else-if + v-else
以下のようなコンポーネントを考えます.
<template>
<div v-if="ok">OK</div>
<p v-else-if="orNot">OR NOT</p>
<span v-else>ELSE</span>
</template>コンパイル結果は以下のようになります.
import { createIf as _createIf, template as _template } from "vue/vapor";
const t0 = _template("<div>OK</div>");
const t1 = _template("<p>OR NOT</p>");
const t2 = _template("<span>ELSE</span>");
function _sfc_render(_ctx) {
const n0 = _createIf(
() => _ctx.ok,
() => {
const n2 = t0();
return n2;
},
() =>
_createIf(
() => _ctx.orNot,
() => {
const n4 = t1();
return n4;
},
() => {
const n7 = t2();
return n7;
}
)
);
return n0;
}v-else-if は createIf のネストによって表現されています.
第三引数に更なる createIf を呼び出す関数が渡されていることがわかります.
コンパイラを読む
IR
まず,v-if 用の IR を確認しましょう.
66export interface IfIRNode extends BaseIRNode {
67 type: IRNodeTypes.IF
68 id: number
69 condition: SimpleExpressionNode
70 positive: BlockIRNode
71 negative?: BlockIRNode | IfIRNode
72 once?: boolean
73}export interface IfIRNode extends BaseIRNode {
type: IRNodeTypes.IF;
id: number;
condition: SimpleExpressionNode;
positive: BlockIRNode;
negative?: BlockIRNode | IfIRNode;
once?: boolean;
}condition: 条件式positive: 条件が真の場合のブロックnegative: 条件が偽の場合のブロック (v-else) またはIfIRNode(v-else-if)once:v-onceと組み合わせた場合
negative が BlockIRNode または IfIRNode のどちらかになっているのがポイントです.v-else の場合は BlockIRNode,v-else-if の場合は IfIRNode になります.
Transformer
transformVIf は createStructuralDirectiveTransform を使って定義されています.
22export const transformVIf: NodeTransform = createStructuralDirectiveTransform(
23 ['if', 'else', 'else-if'],
24 processIf,
25)export const transformVIf: NodeTransform = createStructuralDirectiveTransform(
["if", "else", "else-if"],
processIf
);processIf 関数では,v-if,v-else,v-else-if のそれぞれのケースを処理しています.
v-if の場合
41 if (dir.name === 'if') {
42 const id = context.reference()
43 context.dynamic.flags |= DynamicFlag.INSERT
44 const [branch, onExit] = createIfBranch(node, context)
45
46 return () => {
47 onExit()
48 context.registerOperation({
49 type: IRNodeTypes.IF,
50 id,
51 condition: dir.exp!,
52 positive: branch,
53 once: context.inVOnce,
54 })
55 }if (dir.name === "if") {
const id = context.reference();
context.dynamic.flags |= DynamicFlag.INSERT;
const [branch, onExit] = createIfBranch(node, context);
return () => {
onExit();
context.registerOperation({
type: IRNodeTypes.IF,
id,
condition: dir.exp!,
positive: branch,
once: context.inVOnce,
});
};
}createIfBranch でブロックを作成し,registerOperation で IfIRNode を登録しています.
v-else / v-else-if の場合
56 } else {
57 // check the adjacent v-if
58 const siblingIf = getSiblingIf(context, true)
59
60 const { operation } = context.block
61 let lastIfNode = operation[operation.length - 1]
62
63 if (
64 // check if v-if is the sibling node
65 !siblingIf ||
66 // check if IfNode is the last operation and get the root IfNode
67 !lastIfNode ||
68 lastIfNode.type !== IRNodeTypes.IF
69 ) {
70 context.options.onError(
71 createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc),
72 )
73 return
74 }
75
76 while (lastIfNode.negative && lastIfNode.negative.type === IRNodeTypes.IF) {
77 lastIfNode = lastIfNode.negative
78 }
79
80 // Check if v-else was followed by v-else-if
81 if (dir.name === 'else-if' && lastIfNode.negative) {
82 context.options.onError(
83 createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc),
84 )
85 }
86
87 // TODO ignore comments if the v-if is direct child of <transition> (PR #3622)
88 if (__DEV__ && context.root.comment.length) {
89 node = wrapTemplate(node, ['else-if', 'else'])
90 context.node = node = extend({}, node, {
91 children: [...context.comment, ...node.children],
92 })
93 }
94 context.root.comment = []
95
96 const [branch, onExit] = createIfBranch(node, context)
97
98 if (dir.name === 'else') {
99 lastIfNode.negative = branch
100 } else {
101 lastIfNode.negative = {
102 type: IRNodeTypes.IF,
103 id: -1,
104 condition: dir.exp!,
105 positive: branch,
106 once: context.inVOnce,
107 }
108 }
109
110 return () => onExit()
111 }v-else と v-else-if の場合は,隣接する v-if を探し,その negative プロパティにブロックを追加します.
// 隣接する v-if を探す
const siblingIf = getSiblingIf(context, true);
// 最後の IfNode を取得
let lastIfNode = operation[operation.length - 1];
// v-else-if の場合はネストした IfIRNode を探す
while (lastIfNode.negative && lastIfNode.negative.type === IRNodeTypes.IF) {
lastIfNode = lastIfNode.negative;
}
const [branch, onExit] = createIfBranch(node, context);
if (dir.name === "else") {
// v-else の場合は BlockIRNode を設定
lastIfNode.negative = branch;
} else {
// v-else-if の場合は新しい IfIRNode を設定
lastIfNode.negative = {
type: IRNodeTypes.IF,
id: -1,
condition: dir.exp!,
positive: branch,
once: context.inVOnce,
};
}Codegen
genIf 関数でコード生成を行います.
1import type { CodegenContext } from '../generate'
2import { IRNodeTypes, type IfIRNode } from '../ir'
3import { genBlock } from './block'
4import { genExpression } from './expression'
5import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils'
6
7export function genIf(
8 oper: IfIRNode,
9 context: CodegenContext,
10 isNested = false,
11): CodeFragment[] {
12 const { vaporHelper } = context
13 const { condition, positive, negative, once } = oper
14 const [frag, push] = buildCodeFragment()
15
16 const conditionExpr: CodeFragment[] = [
17 '() => (',
18 ...genExpression(condition, context),
19 ')',
20 ]
21
22 let positiveArg = genBlock(positive, context)
23 let negativeArg: false | CodeFragment[] = false
24
25 if (negative) {
26 if (negative.type === IRNodeTypes.BLOCK) {
27 negativeArg = genBlock(negative, context)
28 } else {
29 negativeArg = ['() => ', ...genIf(negative!, context, true)]
30 }
31 }
32
33 if (!isNested) push(NEWLINE, `const n${oper.id} = `)
34 push(
35 ...genCall(
36 vaporHelper('createIf'),
37 conditionExpr,
38 positiveArg,
39 negativeArg,
40 once && 'true',
41 ),
42 )
43
44 return frag
45}export function genIf(
oper: IfIRNode,
context: CodegenContext,
isNested = false
): CodeFragment[] {
const { vaporHelper } = context;
const { condition, positive, negative, once } = oper;
const conditionExpr: CodeFragment[] = [
"() => (",
...genExpression(condition, context),
")",
];
let positiveArg = genBlock(positive, context);
let negativeArg: false | CodeFragment[] = false;
if (negative) {
if (negative.type === IRNodeTypes.BLOCK) {
// v-else の場合
negativeArg = genBlock(negative, context);
} else {
// v-else-if の場合は再帰的に genIf を呼び出す
negativeArg = ["() => ", ...genIf(negative!, context, true)];
}
}
if (!isNested) push(NEWLINE, `const n${oper.id} = `);
push(
...genCall(
vaporHelper("createIf"),
conditionExpr,
positiveArg,
negativeArg,
once && "true"
)
);
return frag;
}negative が IfIRNode の場合は再帰的に genIf を呼び出すことで,ネストした createIf を生成しています.
ランタイムを読む
createIf は runtime-vapor の apiCreateIf.ts に実装されています.
1import { type Block, type Fragment, fragmentKey } from './apiRender'
2import { getCurrentScope } from '@vue/reactivity'
3import { createComment, createTextNode, insert, remove } from './dom/element'
4import { currentInstance } from './component'
5import { warn } from './warning'
6import { BlockEffectScope, isRenderEffectScope } from './blockEffectScope'
7import {
8 createChildFragmentDirectives,
9 invokeWithMount,
10 invokeWithUnmount,
11 invokeWithUpdate,
12} from './directivesChildFragment'
13
14type BlockFn = () => Block
15
16/*! #__NO_SIDE_EFFECTS__ */
17export const createIf = (
18 condition: () => any,
19 b1: BlockFn,
20 b2?: BlockFn,
21 once?: boolean,
22 // hydrationNode?: Node,
23): Fragment => {
24 let newValue: any
25 let oldValue: any
26 let branch: BlockFn | undefined
27 let parent: ParentNode | undefined | null
28 let block: Block | undefined
29 let scope: BlockEffectScope | undefined
30 const parentScope = getCurrentScope()!
31 const anchor = __DEV__ ? createComment('if') : createTextNode()
32 const fragment: Fragment = {
33 nodes: [],
34 anchor,
35 [fragmentKey]: true,
36 }
37
38 const instance = currentInstance!
39 if (__DEV__ && (!instance || !isRenderEffectScope(parentScope))) {
40 warn('createIf() can only be used inside setup()')
41 }
42
43 // TODO: SSR
44 // if (isHydrating) {
45 // parent = hydrationNode!.parentNode
46 // setCurrentHydrationNode(hydrationNode!)
47 // }
48
49 createChildFragmentDirectives(
50 anchor,
51 () => (scope ? [scope] : []),
52 // source getter
53 condition,
54 // init cb
55 getValue => {
56 newValue = !!getValue()
57 doIf()
58 },
59 // effect cb
60 getValue => {
61 if ((newValue = !!getValue()) !== oldValue) {
62 doIf()
63 } else if (scope) {
64 invokeWithUpdate(scope)
65 }
66 },
67 once,
68 )
69
70 // TODO: SSR
71 // if (isHydrating) {
72 // parent!.insertBefore(anchor, currentHydrationNode)
73 // }
74
75 return fragment
76
77 function doIf() {
78 parent ||= anchor.parentNode
79 if (block) {
80 invokeWithUnmount(scope!, () => remove(block!, parent!))
81 }
82 if ((branch = (oldValue = newValue) ? b1 : b2)) {
83 scope = new BlockEffectScope(instance, parentScope)
84 fragment.nodes = block = scope.run(branch)!
85 invokeWithMount(scope, () => parent && insert(block!, parent, anchor))
86 } else {
87 scope = block = undefined
88 fragment.nodes = []
89 }
90 }
91}export const createIf = (
condition: () => any,
b1: BlockFn,
b2?: BlockFn,
once?: boolean
): Fragment => {
let newValue: any;
let oldValue: any;
let branch: BlockFn | undefined;
let parent: ParentNode | undefined | null;
let block: Block | undefined;
let scope: BlockEffectScope | undefined;
const parentScope = getCurrentScope()!;
const anchor = __DEV__ ? createComment("if") : createTextNode();
const fragment: Fragment = {
nodes: [],
anchor,
[fragmentKey]: true,
};
// ...
createChildFragmentDirectives(
anchor,
() => (scope ? [scope] : []),
condition,
// init cb
(getValue) => {
newValue = !!getValue();
doIf();
},
// effect cb
(getValue) => {
if ((newValue = !!getValue()) !== oldValue) {
doIf();
} else if (scope) {
invokeWithUpdate(scope);
}
},
once
);
return fragment;
function doIf() {
parent ||= anchor.parentNode;
if (block) {
invokeWithUnmount(scope!, () => remove(block!, parent!));
}
if ((branch = (oldValue = newValue) ? b1 : b2)) {
scope = new BlockEffectScope(instance, parentScope);
fragment.nodes = block = scope.run(branch)!;
invokeWithMount(scope, () => parent && insert(block!, parent, anchor));
} else {
scope = block = undefined;
fragment.nodes = [];
}
}
};仕組み
アンカーの作成: 開発環境では
createComment('if')でコメントノード,本番環境ではcreateTextNode()で空のテキストノードを作成します.これは DOM の挿入位置を示すマーカーとして機能します.Fragment の作成:
v-ifの結果はFragmentとして返されます.これはnodes(実際の DOM ノード) とanchor(挿入位置マーカー) を持つオブジェクトです.リアクティブな更新:
createChildFragmentDirectivesを使って,条件の変化を監視します.条件が変化するとdoIf関数が呼び出されます.doIf 関数: 条件の真偽に応じて,適切なブランチ (
b1またはb2) を実行します.- 既存のブロックがある場合は
removeで削除 - 新しいブランチがある場合は
BlockEffectScope内で実行し,insertで DOM に挿入
- 既存のブロックがある場合は
BlockEffectScope
各ブランチは独自の BlockEffectScope 内で実行されます.
これにより:
- ブランチ内の effect は適切にクリーンアップされる
- ブランチの切り替え時に,古いブランチの effect は自動的に停止される
- ライフサイクルフック (mount/unmount) が適切に呼び出される
v-if は v-show と異なり,条件に応じて実際に DOM を生成・破棄するため,実装がより複雑になっています.
しかし,IR の設計とランタイムの Fragment パターンにより,v-else-if のようなネストした条件分岐も elegantly に処理できています.
