v-if Directive
Consider the following component:
<script setup>
import { ref } from "vue";
const ok = ref(true);
</script>
<template>
<div v-if="ok">Hello, v-if!</div>
</template>Compilation Result and Overview
The compilation result is as follows:
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;
}Unlike v-show, v-if creates and destroys DOM elements based on the condition.
Therefore, it uses the createIf helper function to implement conditional rendering.
createIf takes the condition as the first argument, the block to execute when the condition is true (positive) as the second argument, and the block to execute when the condition is false (negative) as the third argument.
v-if + v-else
Consider the following component:
<template>
<div v-if="ok">YES</div>
<p v-else>NO</p>
</template>The compilation result is as follows:
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;
}The v-else block is passed as the third argument.
v-if + v-else-if + v-else
Consider the following component:
<template>
<div v-if="ok">OK</div>
<p v-else-if="orNot">OR NOT</p>
<span v-else>ELSE</span>
</template>The compilation result is as follows:
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 is expressed through nested createIf calls.
You can see that the third argument contains a function that calls another createIf.
Reading the Compiler
IR
First, let's look at the IR for v-if.
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: The condition expressionpositive: The block when the condition is truenegative: The block when the condition is false (v-else) orIfIRNode(v-else-if)once: When combined withv-once
The key point is that negative can be either a BlockIRNode or an IfIRNode.
For v-else, it's a BlockIRNode, and for v-else-if, it's an IfIRNode.
Transformer
transformVIf is defined using createStructuralDirectiveTransform.
22export const transformVIf: NodeTransform = createStructuralDirectiveTransform(
23 ['if', 'else', 'else-if'],
24 processIf,
25)export const transformVIf: NodeTransform = createStructuralDirectiveTransform(
["if", "else", "else-if"],
processIf
);The processIf function handles each case of v-if, v-else, and v-else-if.
For 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,
});
};
}It creates a block with createIfBranch and registers the IfIRNode with registerOperation.
For 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 }For v-else and v-else-if, it finds the adjacent v-if and adds a block to its negative property.
// Find the adjacent v-if
const siblingIf = getSiblingIf(context, true);
// Get the last IfNode
let lastIfNode = operation[operation.length - 1];
// For v-else-if, find the nested IfIRNode
while (lastIfNode.negative && lastIfNode.negative.type === IRNodeTypes.IF) {
lastIfNode = lastIfNode.negative;
}
const [branch, onExit] = createIfBranch(node, context);
if (dir.name === "else") {
// For v-else, set BlockIRNode
lastIfNode.negative = branch;
} else {
// For v-else-if, set a new IfIRNode
lastIfNode.negative = {
type: IRNodeTypes.IF,
id: -1,
condition: dir.exp!,
positive: branch,
once: context.inVOnce,
};
}Codegen
The genIf function handles code generation.
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) {
// For v-else
negativeArg = genBlock(negative, context);
} else {
// For v-else-if, recursively call 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;
}When negative is an IfIRNode, it recursively calls genIf to generate nested createIf calls.
Reading the Runtime
createIf is implemented in apiCreateIf.ts in runtime-vapor.
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 = [];
}
}
};How It Works
Anchor Creation: In development, it creates a comment node with
createComment('if'), and in production, an empty text node withcreateTextNode(). This serves as a marker for the DOM insertion position.Fragment Creation: The result of
v-ifis returned as aFragment. This is an object that hasnodes(the actual DOM nodes) andanchor(the insertion position marker).Reactive Updates: It uses
createChildFragmentDirectivesto watch for changes in the condition. When the condition changes, thedoIffunction is called.doIf Function: Based on the condition's truth value, it executes the appropriate branch (
b1orb2).- If there's an existing block, it removes it with
remove - If there's a new branch, it executes it within a
BlockEffectScopeand inserts it into the DOM withinsert
- If there's an existing block, it removes it with
BlockEffectScope
Each branch is executed within its own BlockEffectScope.
This ensures:
- Effects within the branch are properly cleaned up
- When switching branches, the old branch's effects are automatically stopped
- Lifecycle hooks (mount/unmount) are called appropriately
Unlike v-show, v-if actually creates and destroys the DOM based on the condition, making the implementation more complex.
However, through the IR design and the runtime's Fragment pattern, even nested conditional branches like v-else-if can be handled elegantly.
