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アプリケーションをより予測可能で保守しやすくします。このシステムの優雅さは、依存関係を自動的に追跡し、更新を正確に局所化する能力にあり、これにより非常に最適化されたパフォーマンスの高いユーザーインターフェースが実現されます。