Skip to content

v-if Directive

Consider the following component:

vue
<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:

js
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:

vue
<template>
  <div v-if="ok">YES</div>
  <p v-else>NO</p>
</template>

The compilation result is as follows:

js
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:

vue
<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:

js
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}
ts
export interface IfIRNode extends BaseIRNode {
  type: IRNodeTypes.IF;
  id: number;
  condition: SimpleExpressionNode;
  positive: BlockIRNode;
  negative?: BlockIRNode | IfIRNode;
  once?: boolean;
}
  • condition: The condition expression
  • positive: The block when the condition is true
  • negative: The block when the condition is false (v-else) or IfIRNode (v-else-if)
  • once: When combined with v-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)
ts
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    }
ts
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.

ts
// 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}
ts
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}
ts
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

  1. Anchor Creation: In development, it creates a comment node with createComment('if'), and in production, an empty text node with createTextNode(). This serves as a marker for the DOM insertion position.

  2. Fragment Creation: The result of v-if is returned as a Fragment. This is an object that has nodes (the actual DOM nodes) and anchor (the insertion position marker).

  3. Reactive Updates: It uses createChildFragmentDirectives to watch for changes in the condition. When the condition changes, the doIf function is called.

  4. doIf Function: Based on the condition's truth value, it executes the appropriate branch (b1 or b2).

    • If there's an existing block, it removes it with remove
    • If there's a new branch, it executes it within a BlockEffectScope and inserts it into the DOM with insert

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.