📅

Vue の Composable、内部で何が起きてるのか追ってみた!

2024/12/14に公開

この記事は、📅 Vue Advent Calendar 2024 の 14 日目の記事です。

1. はじめに

はじめまして、haba です。

Vue 3 から登場した「Composable」をご存知でしょうか?

Vue アプリケーションの文脈で「コンポーザブル(composable)」とは、Vue の Composition API を活用して状態を持つロジックをカプセル化して再利用するための関数です。
公式ドキュメントより)

Composable は、状態やロジックを簡潔にまとめ、再利用可能な形で Vue コンポーネント間で共有できる便利な仕組みです。例えば、VueUse のようなライブラリには、数多くの便利な Composable が提供されています。これらを活用することで、開発効率を大幅に向上させることができます。

この記事では、Composable の「使い方」ではなく、「内部の動き」と「コンパイル時の最適化プロセス」 にフォーカスします。Vue のリアクティブシステムやコンパイラの動作を掘り下げることで、独自の Composable をより効率的に設計するための知見を提供します。

この記事を読めば、Vue の内部動作の魅力を発見し、実際の開発に役立つアイデアを得られるはずです。それでは、一緒に探求していきましょう!

2. Composable の基本的な使い方

まずは、Composable の基本構造と使い方をおさらいしましょう。Composable は、状態管理やロジックをカプセル化し、再利用可能にするための仕組みです。以下に、シンプルなカウンター機能を提供する useCounter の例を示します。

useCounter のコード例

// composables/useCounter.ts
import { ref } from "vue";

export function useCounter(initialValue = 0) {
  const count = ref(initialValue);

  const increment = () => {
    count.value++;
  };

  const decrement = () => {
    count.value--;
  };

  const reset = () => {
    count.value = initialValue;
  };

  return {
    count,
    increment,
    decrement,
    reset,
  };
}

提供する機能

  • count: 現在のカウント値を保持するリアクティブなデータ。
  • increment: カウントを 1 増やします。
  • decrement: カウントを 1 減らします。
  • reset: カウントを初期値にリセットします。

Composable をコンポーネントで使ってみる

useCounter を Vue コンポーネント内で利用する例を見てみましょう。

<script setup lang="ts">
import { useCounter } from "./composables/useCounter";

const { count, increment, decrement, reset } = useCounter(0);
</script>

<template>
  <div>
    <h1>カウンター: {{ count }}</h1>
    <button @click="increment">増加</button>
    <button @click="decrement">減少</button>
    <button @click="reset">リセット</button>
  </div>
</template>

実際の使用場面とメリット

このような Composable を使用することで、次のようなメリットが得られます:

  1. ロジックの再利用
    例えば、Composable を利用することで、複数の画面で同じカウントロジックを簡単に導入できます。

  2. テストの容易さ
    ロジックが関数として分離されているため、単体テストをシンプルに行えます。

  3. コードの見通しの良さ
    状態とロジックが明確にカプセル化されており、メンテナンスが容易になります。

Composable を理解する第一歩として、こうした小さな例を試してみるのがおすすめです。

3. setup 関数からどう見えるのか?

Composable を使ったロジックが、Vue コンポーネントの setup 関数でどのように動作し、テンプレートに渡されるのかを詳しく見ていきましょう。

setup 内での動き

以下は、setup 関数で useCounter を利用した場合のコードです。

setup(__props, { expose: __expose }) {
  __expose();
  const { count, increment, decrement, reset } = useCounter(0);

  const __returned__ = { count, increment, decrement, reset };

  Object.defineProperty(__returned__, "__isScriptSetup", {
    enumerable: false,
    value: true,
  });

  return __returned__;
}

ポイント解説

  • __returned__
    このオブジェクトは setup 関数で準備されたデータやメソッドをテンプレートに公開する役割を担います。例えば、count はテンプレートで動的に値を表示する際に利用されます。

  • リアクティブな更新
    countref で定義されているため、UI が自動的に更新されます。これが Vue のリアクティブシステムの利点です。

4. Vue のリアクティブシステムと内部実装

Composableの動作を理解するために、Vue の内部実装を詳しく見ていきましょう。

setup 関数の内部動作

Composableを使用したコンポーネントでは、内部で以下のようなsetup関数が生成されます

setup(__props, { expose: __expose }) {
    // 親コンポーネントからのアクセス制御
    __expose();

    // Composableの呼び出し
    const { count, increment, decrement, reset } = useCounter(0);

    // テンプレートに公開するオブジェクトの準備
    const __returned__ = { count, increment, decrement, reset };

    // <script setup>構文の使用を示すフラグ設定
    Object.defineProperty(__returned__, "__isScriptSetup", {
        enumerable: false,
        value: true
    });

    return __returned__;
}

重要なポイント

  1. __exposeメソッド
    • 親コンポーネントからアクセス可能なプロパティを制御
    • 引数なしの場合は内部状態を隠蔽
    • 特定のメソッドのみを公開することも可能:
__expose({ increment, reset });
  1. 戻り値オブジェクト
    • テンプレートで使用する値とメソッドをまとめて返却
    • __isScriptSetupフラグにより、コンパイラが最適化を実行

render 関数の生成

テンプレートは、以下のようなrender関数にコンパイルされます:

function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", _hoisted_1, [
    _createElementVNode("h1", null, "カウンター: " + _toDisplayString($setup.count), 1 /* TEXT */),
    _createElementVNode("div", _hoisted_2, [
      _createElementVNode("button", {
        onClick: _cache[0] || (_cache[0] = (...args) => ($setup.increment && $setup.increment(...args)))
      }, "増加"),
      _createElementVNode("button", {
        onClick: _cache[1] || (_cache[1] = (...args) => ($setup.decrement && $setup.decrement(...args)))
      }, "減少"),
      _createElementVNode("button", {
        onClick: _cache[2] || (_cache[2] = (...args) => ($setup.reset && $setup.reset(...args)))
      }, "リセット")
    ])
  ]))
}

render関数の特徴

  1. 最適化されたDOM操作

    • _openBlock()_createElementBlock()による効率的なパッチング
    • イベントハンドラのキャッシュ機構
  2. リアクティブな更新

    • $setup.countの変更を検知して自動的にDOM更新
    • Virtual DOMの差分計算による効率的な再描画

リアクティブシステムの動作

Composableの状態管理は、Vueのリアクティブシステムによって支えられています:

// 内部的な依存関係の追跡
const count = ref(0);        // リアクティブな値の作成
const double = computed(() => count.value * 2);  // 依存関係の自動追跡

// 値の更新時
count.value++;  // → double の再計算とUIの更新がトリガー

このシステムにより:

  • 依存関係の自動追跡
  • 必要な箇所のみの効率的な更新
  • パフォーマンスの最適化

が実現されています。

5. まとめ

Vue の Composable の内部を深掘りしてみると、以下のようなことがわかりました。

  1. 最適化された実行基盤

    • setup関数による効率的な状態管理
    • コンパイル時の最適化による高速な実行
  2. 洗練された設計

    • リアクティブシステムによる状態同期
    • Virtual DOMによる効率的な更新
  3. 実践的な応用

    • コンポーネント間でのロジック共有
    • テスト容易性の向上

Vue の内部実装を理解することで、より効率的な Composable の設計が可能になります。ぜひ、これらの知見を活かして、より洗練された Vue アプリケーションを開発してみてください!

GitHubで編集を提案
Vue・Nuxt 情報が集まる広場 / Plaza for Vue・Nuxt.

Discussion