Skip to content

v-if ディレクティブ

以下のようなコンポーネントを考えます.

vue
<script setup>
import { ref } from "vue";
const ok = ref(true);
</script>

<template>
  <div v-if="ok">Hello, v-if!</div>
</template>

コンパイル結果と概要

コンパイル結果は以下のようになります.

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;
}

v-show とは異なり,v-if は条件に応じて DOM 要素を生成・破棄します.
そのため,createIf というヘルパー関数を使って,条件分岐を実現しています.

createIf は第一引数に条件,第二引数に条件が真の場合に実行するブロック (positive),第三引数に条件が偽の場合に実行するブロック (negative) を取ります.

v-if + v-else

以下のようなコンポーネントを考えます.

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

コンパイル結果は以下のようになります.

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;
}

第三引数に v-else のブロックが渡されています.

v-if + v-else-if + v-else

以下のようなコンポーネントを考えます.

vue
<template>
  <div v-if="ok">OK</div>
  <p v-else-if="orNot">OR NOT</p>
  <span v-else>ELSE</span>
</template>

コンパイル結果は以下のようになります.

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-ifcreateIf のネストによって表現されています.
第三引数に更なる 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}
ts
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 と組み合わせた場合

negativeBlockIRNode または IfIRNode のどちらかになっているのがポイントです.
v-else の場合は BlockIRNodev-else-if の場合は IfIRNode になります.

Transformer

transformVIfcreateStructuralDirectiveTransform を使って定義されています.

22export const transformVIf: NodeTransform = createStructuralDirectiveTransform(
23  ['if', 'else', 'else-if'],
24  processIf,
25)
ts
export const transformVIf: NodeTransform = createStructuralDirectiveTransform(
  ["if", "else", "else-if"],
  processIf
);

processIf 関数では,v-ifv-elsev-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    }
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,
    });
  };
}

createIfBranch でブロックを作成し,registerOperationIfIRNode を登録しています.

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-elsev-else-if の場合は,隣接する v-if を探し,その negative プロパティにブロックを追加します.

ts
// 隣接する 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}
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) {
      // 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;
}

negativeIfIRNode の場合は再帰的に genIf を呼び出すことで,ネストした createIf を生成しています.

ランタイムを読む

createIfruntime-vaporapiCreateIf.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}
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 = [];
    }
  }
};

仕組み

  1. アンカーの作成: 開発環境では createComment('if') でコメントノード,本番環境では createTextNode() で空のテキストノードを作成します.これは DOM の挿入位置を示すマーカーとして機能します.

  2. Fragment の作成: v-if の結果は Fragment として返されます.これは nodes (実際の DOM ノード) と anchor (挿入位置マーカー) を持つオブジェクトです.

  3. リアクティブな更新: createChildFragmentDirectives を使って,条件の変化を監視します.条件が変化すると doIf 関数が呼び出されます.

  4. doIf 関数: 条件の真偽に応じて,適切なブランチ (b1 または b2) を実行します.

    • 既存のブロックがある場合は remove で削除
    • 新しいブランチがある場合は BlockEffectScope 内で実行し,insert で DOM に挿入

BlockEffectScope

各ブランチは独自の BlockEffectScope 内で実行されます.
これにより:

  • ブランチ内の effect は適切にクリーンアップされる
  • ブランチの切り替え時に,古いブランチの effect は自動的に停止される
  • ライフサイクルフック (mount/unmount) が適切に呼び出される

v-ifv-show と異なり,条件に応じて実際に DOM を生成・破棄するため,実装がより複雑になっています.
しかし,IR の設計とランタイムの Fragment パターンにより,v-else-if のようなネストした条件分岐も elegantly に処理できています.