Vue 3 リアクティブシステムの解明 - ref、reactive、effectの深掘り
Emily Parker
Product Engineer · Leapcell

はじめに
現代のウェブ開発の世界では、ダイナミックで応答性の高いユーザーインターフェースを作成することが最重要です。Vue.jsのようなフレームワークは、アプリケーションの状態を管理し、更新を効率的にレンダリングするための強力なツールを提供することで、このプロセスを大幅に簡素化しました。Vue 3のエレガンスの核心には、その洗練されたリアクティブシステムがあります。これは、依存関係を自動的に追跡し、必要な場合にのみコンポーネントを再レンダリングする洗練されたメカニズムです。このシステムを理解することは、パフォーマンスが高く、保守可能で、予測可能なVueアプリケーションを作成するために不可欠です。この記事では、Vue 3のリアクティブシステムの基盤となる原則を、基本要素である ref
、reactive
、effect
に焦点を当てて徹底的に探求します。
リアクティビティのコアコンセプト
特定のAPIに飛び込む前に、Vue 3のリアクティブシステムの基盤となる主要な概念を理解しておきましょう。
リアクティブステート (Reactive State): これは、変更されたときにユーザーインターフェースの更新をトリガーする可能性のあるデータです。Vueでは、特定のデータをリアクティブとして明示的に宣言します。
依存関係追跡 (Dependency Tracking): コードのどの部分(例:テンプレートレンダリング、算出プロパティ)が特定のリアクティブステートに依存しているかをシステムが「知る」能力です。
変更検出 (Change Detection): リアクティブステートへの変更を監視するメカニズムです。
副作用(またはエフェクト、Side Effect/Effect): リアクティブステートを読み取り、そのステートが変更されたときに再実行する必要がある可能性のある操作です。最も一般的なエフェクトは、コンポーネントのテンプレートのレンダリングです。
これらの概念を念頭に置いて、ref
と reactive
がどのようにリアクティブステートを作成し、effect
が更新をどのようにオーケストレーションするかを探ってみましょう。
リアクティブステートの作成
Vue 3は、リアクティブステートを宣言するための主に2つの方法、ref
と reactive
を提供します。どちらを選択するかは、扱っているデータの種類に依存することがよくあります。
プリミティブ値のための ref
ref
は、主に数値、文字列、ブール値のようなプリミティブ値、または単一のオブジェクトへのリアクティブな参照を作成するために使用されます。値は .value
プロパティを持つオブジェクトにラップされます。このラッパーにより、リアクティブシステムは基盤となるプリミティブへの変更を追跡できます。
import { ref } from 'vue'; // リアクティブな数値の作成 const count = ref(0); console.log(count.value); // 0 // リアクティブな数値の変更 count.value++; console.log(count.value); // 1 // Vueコンポーネントでの例 // <template> // <p>Count: {{ count }}</p> // <button @click="count++">Increment</button> // </template> // <script setup> // import { ref } from 'vue'; // const count = ref(0); // </script>
テンプレートまたは effect
で count.value
がアクセスされると、Vueのリアクティブシステムは依存関係を「登録」します。その後 count.value
が変更されると、システムは登録されたすべての依存関係に「通知」し、それらの再実行をトリガーします。
オブジェクトと配列のための reactive
reactive
は、リアクティブなオブジェクトと配列を作成するために使用されます。プレーンなJavaScriptオブジェクトをリアクティブなプロキシに変換します。このオブジェクト内のネストされたプロパティや配列内の要素もリアクティブになります。
import { reactive } from 'vue'; // リアクティブなオブジェクトの作成 const user = reactive({ name: 'Alice', age: 30, address: { street: '123 Main St', city: 'Anytown' } }); console.log(user.name); // Alice // プロパティの変更 user.age++; console.log(user.age); // 31 // ネストされたプロパティの変更 user.address.city = 'Newcity'; console.log(user.address.city); // Newcity // Vueコンポーネントでの例 // <template> // <p>Name: {{ user.name }}</p> // <p>City: {{ user.address.city }}</p> // <button @click="user.age++">Grow Older</button> // </template> // <script setup> // import { reactive } from 'vue'; // const user = reactive({ // name: 'Bob', // age: 25, // address: { city: 'Oldcity' } // }); // </script>
reactive
は内部的にJavaScriptの Proxy
オブジェクトを使用します。リアクティブなオブジェクトのプロパティにアクセスまたは変更すると、Proxy
がこれらの操作をインターセプトします。これにより、Vueは依存関係追跡(プロパティが読み取られたとき)と更新のトリガー(プロパティが書き込まれたとき)を実行できます。
ref
と reactive
の関係
setup
関数内で、スタンドアロンのリアクティブステートとして意図されているオブジェクトに対して ref
が使用されることはよくあります。ref
がオブジェクトを保持している場合、Vueは内部的に reactive
を使用してそのオブジェクトを自動的にリアクティブにします。したがって、ref({ data: 'value' })
は reactive({ data: 'value' })
と機能的に似ていますが、前者は依然として .value
アクセスが必要です。
reactive
はリアクティブなオブジェクトを直接公開しますが、ref
はプリミティブとオブジェクトの両方に対して一貫したインターフェース(.value
)を提供し、特にリアクティブな値を渡す際に一部のパターンを簡素化できます。
オーケストレーター effect
リアクティビティが実際に「何かをする」方法の中心にあるのは effect
関数です。アプリケーション開発のために直接公開されることはめったにありません(ライブラリを構築したり、リアクティビティを深く掘り下げたりしない限り)が、基盤となるメカニズムを理解するためには不可欠です。effect
は関数を引数として受け取り、その関数は即座に実行され、その中でアクセスされたリアクティブな依存関係が変更されるたびに再実行されます。
Vueのレンダリングメカニズム(テンプレートコンパイル)は、暗黙的に effect
を作成します。コンポーネントの <template>
を定義すると、Vueはそれをレンダー関数にコンパイルします。このレンダー関数は effect
になります。テンプレート内で count.value
が読み取られると、レンダー関数は count
を依存関係として登録します。count.value
が変更されると、effect
(レンダー関数)が再実行され、コンポーネントの再レンダリングがトリガーされます。
effect
がこれをどのように実現するかを簡略化したバージョンで示しましょう:
// これは概念的な表現であり、直接実行可能なVueコードではありません // 単純化されたリアクティブコアを想像してください let activeEffect = null; const targetMap = new WeakMap(); // targetオブジェクトをpropsのMapにMapし、そのMapをeffectsのSetにMapする function track(target, key) { if (activeEffect) { let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } dep.add(activeEffect); } } function trigger(target, key) { const depsMap = targetMap.get(target); if (!depsMap) return; const dep = depsMap.get(key); if (dep) { dep.forEach(effect => effect()); } } // 単純化されたref実装 function ref(raw) { const r = { get value() { track(r, 'value'); return raw; }, set value(newVal) { raw = newVal; trigger(r, 'value'); } }; return r; } // 単純化されたreactive (Proxy) 実装 function reactive(obj) { return new Proxy(obj, { get(target, key, receiver) { track(target, key); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); trigger(target, key); return result; } }); } function effect(fn) { const effectFn = () => { activeEffect = effectFn; // 現在のアクティブなエフェクトを設定 try { return fn(); // 関数を実行し、トラックをトリガーする } finally { activeEffect = null; // アクティブなエフェクトをクリアする } }; effectFn(); // 即座に実行 return effectFn; // 手動停止のために返す (必要であれば) } // --- 使用例 --- const myCount = ref(0); const myUser = reactive({ name: 'Bob' }); effect(() => { console.log(`Count changed: ${myCount.value}`); }); effect(() => { console.log(`User name changed: ${myUser.name}`); }); myCount.value++; // "Count changed: 1" をトリガーする myUser.name = 'Alice'; // "User name changed: Alice" をトリガーする
この単純化されたモデルでは:
effect
が呼び出されると、activeEffect
が現在の関数に設定されます。- 関数内で
myCount.value
がアクセスされると、そのget
ハンドラはtrack
を呼び出します。 track
はactiveEffect
を使用して、現在のeffect
関数をmyCount
(具体的にはその 'value' プロパティ)への依存関係として登録します。- 同様に、
myUser.name
がアクセスされると、そのget
ハンドラ(Proxy
から)はtrack
を呼び出し、'name'
プロパティへの依存関係としてeffect
を登録します。 myCount.value
が更新されると、そのset
ハンドラはtrigger
を呼び出します。trigger
はmyCount
の 'value' プロパティに登録されているすべてのエフェクトを検索し、それらを再実行します。myUser.name
が更新されたときも、同様のロジックが適用されます。
ref
と reactive
プロキシ内の track
と trigger
の絶え間ない相互作用は、effect
ランナーによってオーケストレーションされ、Vueの非常に効率的で自動的なリアクティブシステムのバックボーンを形成します。
結論
ref
、reactive
、および基盤となる effect
メカニズムに基づいて構築されたVue 3のリアクティブシステムは、アプリケーションの状態を管理し、UIの一貫性を確保するための強力で直感的な方法を提供します。プリミティブ値には ref
を、オブジェクトと配列には reactive
を利用することで、開発者は依存関係のあるエフェクト(コンポーネントレンダリングを含む)を自動的にトリガーするリアクティブステートを宣言できます。この宣言的なアプローチは複雑な状態管理を大幅に簡素化し、Vueアプリケーションをより予測可能で保守しやすくします。このシステムの優雅さは、依存関係を自動的に追跡し、更新を正確に局所化する能力にあり、これにより非常に最適化されたパフォーマンスの高いユーザーインターフェースが実現されます。