Vue 3 のリアクティブ性の原理を徹底解説:プロキシのインターセプト + 依存関係の収集 + 更新のトリガー
Vue 3 のリアクティビティの仕組みを理解することで、より効率的なコードを記述したり、厄介なリアクティビティの問題をデバッグしたりできるようになります。このレッスンでは、Vue 3 のリアクティビティシステムの基盤となる実装、すなわちプロキシインターセプター、依存関係の解決、更新トリガー、そして ref、計算プロパティ、ウォッチの内部メカニズムについて詳しく解説します。
Vue 3では、Vue 2のObject.definePropertyがES6のProxyに置き換えられ、配列、Map、Setに対するネイティブサポートが提供されるとともに、パフォーマンスが2倍に向上しています。その仕組みを理解すれば、リアクティブデータに関連する5つのよくある落とし穴を回避できるようになります。
1. 学習内容
- プロキシが get/set 操作をどのように傍受するか
- リアクティブプログラミングにおけるReflect APIの役割
- コアプロセス:追跡(依存関係の収集)+トリガー(更新のトリガー)
ref、reactive、およびcomputedの内部実装- watch と watchEffect の内部的な違い
- パフォーマンスの最適化:shallowRef / markRaw
- レスポンシブデザインが失敗する5つの主な原因
2. 10万行のテーブルで発生した「フリーズ」事象
(1) 課題:10万件のアイテムのリストをrefで囲むと、ページがフリーズしてしまう
アリスは、管理者用に10,000行の製品テーブルを読み込みました:
// ❌ The "Flip" Version:deep reactive Processing 10 Ten Thousand Lines
import { ref } from 'vue'
const products = ref([
{ id: 1, name: 'Product 1', price: 99, ... },
// ... 10,000 items ...
])
// Vue For each property、All nested objects are created Proxy
// Memory Usage:100MB+
// First Render: 3s
// Scroll:10 FPS(Lag)
パフォーマンスモニターには次のように表示されます:
「10,000行 × 4つのネストされたプロパティ × プロキシのオーバーヘッド = 40,000個のプロキシ。プロパティへのアクセスごとにトラッキングがトリガーされる。メモリ:100MB。FPS:10。」
(2) Vue の shallowRef と markRaw を使った解決策
import { shallowRef, markRaw } from 'vue'
// ✅ shallowRef:Follow Only .value Replace All
const products = shallowRef([
{ id: 1, name: 'Product 1' },
// ... 10,000 items (Will not be Proxy-wrapped) ...
])
// ✅ markRaw:The tagged object never responds(Fastest)
const staticConfig = markRaw({
apiUrl: 'https://api.example.com',
version: '1.0'
})
// Replace All
products.value = await fetchProducts() // Trigger only 1 Next Update
(3) 収益
最適化後:
- プロキシの数:40,000 → 0(shallowRef はプロキシを作成しない)
- メモリ使用量:100MB → 5MB
- 初回レンダリング:3秒 → 200ミリ秒
- FPS: 10 → 60(スムーズ)
3. プロキシのインターセプト:Vue 3 のリアクティブ性の要
(1) プロキシとは何ですか?
プロキシは、ES6で導入されたオブジェクトラッパーであり、13種類のオブジェクト操作(get、set、has、deletePropertyなど)をインターセプトすることができます。
// Basics Proxy Example
const target = { name: 'Alice', age: 25 }
const proxy = new Proxy(target, {
get(target, key) {
console.log(`Read ${key}`)
return Reflect.get(target, key)
},
set(target, key, value) {
console.log(`Write ${key} = ${value}`)
return Reflect.set(target, key, value)
}
})
proxy.name // Output "Read name",Back 'Alice'
proxy.age = 26 // Output "Write age = 26"
(2) Vue 3のリアクティブ実装(簡略版)
// Vue 3 Inside reactivity A Simplified Implementation of a Package
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 1. Dependency Collection:Track who is using this property
track(target, key)
// 2. Recursive Wrapping:Nested objects also become reactive
const value = Reflect.get(target, key, receiver)
if (isObject(value)) {
return reactive(value)
}
return value
},
set(target, key, value, receiver) {
// 1. Setting Value
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 2. Trigger an update:Notify all locations where this property is used
if (oldValue !== value) {
trigger(target, key)
}
return result
}
})
}
(3) プロキシと Object.defineProperty の比較
| 側面 | Object.defineProperty (Vue 2) | Proxy (Vue 3) |
|---|---|---|
| 配列のサポート | 特別な処理が必要 | ネイティブサポート |
| マップ/セット | ❌ | ✅ |
| 新しいプロパティを追加 | Vue.set が必要 | Auto-react |
| パフォーマンス | 平均 | 向上(レイジープロキシ) |
| ブラウザの対応状況 | IE9以降 | IEでは未対応(ポリフィルが必要) |
4. track + trigger:依存関係の収集と更新のトリガー
(1) フローチャートの全体像
sequenceDiagram
participant T as Template
participant C as Component
participant R as Reactive<br/>(Proxy)
participant W as WeakMap<br/>(target → Map)
participant D as Data
T->>C: 1. Render (Read count)
C->>R: 2. get count
R->>W: 3. track(target, 'count')
W->>W: 4. Record the current activeEffect
R->>D: 5. Read 0
R-->>C: 6. Return 0
C-->>T: 7. Display "Stock: 0"
Note over D,T: The user clicks the button
D->>R: 8. set count = 1
R->>W: 9. trigger(target, 'count')
W->>W: 10. Find all dependencies
R->>C: 11. Scheduling Update
C-->>T: 12. Display "Stock: 1"
(2) グローバル依存関係ストレージの構造
// Vue 3 Internal Data Structures
const targetMap = new WeakMap()
// targetMap = { obj1: Map1, obj2: Map2, ... }
// Map1 = { key1: Set1, key2: Set2, ... }
// Set1 = Set<activeEffect1, activeEffect2, ...>
function track(target, key) {
// 1. Get target Corresponding Map
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 2. Get key Corresponding Set
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
// 3. Record the currently active effect
if (activeEffect) {
dep.add(activeEffect)
}
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
// Notify all dependencies to re-execute
dep.forEach(effect => effect.run())
}
}
(3) 5つのポイント
| 要点 | 説明 |
|---|---|
| 取得時の追跡 | プロパティを読み取る → 「誰が使用しているか」を記録する |
| トリガーが設定されたとき | プロパティを変更 → 「すべてのユーザー」に通知 |
| WeakMapの格納 | 自動ガベージコレクション(対象が破棄されると自動的にクリーンアップされる) |
| activeEffect | 現在実行中のエフェクト(render / watch / computed) |
| バッチ処理 | 複数の変更があっても、更新は1回のみ(nextTick) |
5. refの内部実装
(1) ref のソースコード(簡略版)
// Vue 3 ref Internal Implementation
class RefImpl {
constructor(value) {
this._value = isObject(value) ? reactive(value) : value
}
get value() {
// 1. track
track(this, 'value')
return this._value
}
set value(newValue) {
// 1. Compare the old and new values
if (hasChanged(this._value, newValue)) {
this._value = isObject(newValue) ? reactive(newValue) : newValue
// 2. trigger
trigger(this, 'value')
}
}
}
function ref(value) {
return new RefImpl(value)
}
(2) 自動展開の仕組み
<template>
<!-- Automatic unpacking in templates:{{ count }} rather than {{ count.value }} -->
<p>{{ count }}</p>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0(JS Domestic Demand .value)
console.log(count) // 0(devtools Displayed in the middle 0)
</script>
原則:テンプレートのコンパイル時、コンパイラは自動的に .value を ref に追加します(手動で記述する必要はありません)。
6. computedの内部実装
(1) 怠惰+不潔フラグ
// Vue 3 computed Simplified Implementation
function computed(getter) {
const ref = {
// 1. Cache value
_value: undefined,
// 2. Has it expired?(Needs to be recalculated)
_dirty: true,
// 3. getter
get value() {
// Lazy Evaluation:Counted only on the first visit
if (this._dirty) {
this._value = effect(getter) // Collect Dependencies
this._dirty = false
}
// 4. track(Who is using me?)
track(this, 'value')
return this._value
}
}
// 5. When dependencies change,Mark dirty
effect(() => getter()).scheduled = () => {
ref._dirty = true
trigger(ref, 'value') // For triggering computed Component Updates
}
return ref
}
(2) キャッシュの仕組み
const count = ref(0)
const double = computed(() => count.value * 2)
console.log(double.value) // 0(First Calculation)
console.log(double.value) // 0 (Cached, Do not recalculate)
count.value = 5
console.log(double.value) // 10(Recalculation)
7. パフォーマンスの最適化:shallowRef / shallowReactive / markRaw
(1) shallowRef: 浅い応答
import { shallowRef, triggerRef } from 'vue'
const products = shallowRef([
{ id: 1, name: 'iPhone', price: 999 },
{ id: 2, name: 'MacBook', price: 2499 }
])
// ❌ Not responding(Edit .value Internal Properties)
products.value[0].name = 'iPad'
// Will not trigger an update(shallowRef Follow Only .value Replace All)
// ✅ Trigger an update(Replace All)
products.value = [...products.value, { id: 3, name: 'iPad' }]
// ✅ Force Trigger(Manually after making internal changes trigger)
products.value[0].name = 'iPad'
triggerRef(products)
(2) shallowReactive: 浅いリアクティブ
import { shallowReactive } from 'vue'
const state = shallowReactive({
user: { name: 'Alice' },
settings: { theme: 'dark' }
})
// ✅ Response:Changes to Top-Level Properties
state.user = { name: 'Bob' }
// ❌ Not responding:Nested Properties
state.user.name = 'Charlie'
(3) markRaw:まったく反応しない
import { markRaw, reactive } from 'vue'
// Examples of Third-Party Libraries (e.g. ECharts, Mapbox) No need for reactive design
const map = markRaw(new Map())
const chart = markRaw(echarts.init(element))
const state = reactive({
map, // ✅ Will not be Proxy Packaging
chart // ✅ Performance Improvements 30%
})
(4) 5つの主なユースケース
| シナリオ | 解決策 | パフォーマンスの向上 |
|---|---|---|
| ビッグデータ一覧(100,000件以上) | shallowRef | 50倍 |
| サードパーティ製ライブラリの例 | markRaw | 30% |
| 頻繁なオブジェクトの作成・破棄 | shallowReactive | 20% |
| 読み取り専用設定 | markRaw | 10% |
| 不変データ | shallowRef | 5% |
8. 完全な例:レスポンシブデザインの原則の適用
(1) ▶ サンプル:. 「トラック+トリガー」の簡略版
// Simulation Vue 3 Responsive Systems
const targetMap = new WeakMap()
let activeEffect = null
function track(target, key) {
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
if (activeEffect) {
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())
}
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return Reflect.get(target, key)
},
set(target, key, value) {
const result = Reflect.set(target, key, value)
trigger(target, key)
return result
}
})
}
// Usage
const state = reactive({ count: 0 })
let render = null
// Simulation render Function
activeEffect = () => console.log(`Render: count = ${state.count}`)
render = activeEffect
render() // Output "Render: count = 0"
state.count = 1 // Trigger render,Output "Render: count = 1"
(2) ▶ サンプル:. 5つの主要なパフォーマンス指標の比較
| モード | 10万行での処理性能 | メモリ | 適用対象 |
|---|---|---|---|
ref([]) |
100 ms | 100 MB | 小規模データ |
reactive({...}) |
100 ms | 100 MB | ネストされたオブジェクト |
shallowRef([]) |
5 ms | 5 MB | 大規模リスト |
shallowReactive({...}) |
30 ms | 30 MB | 浅いオブジェクト |
markRaw({...}) |
2 ms | 2 MB | サードパーティのインスタンス |
(3) ▶ サンプル:. ref および reactive の内部構造
import { ref, reactive, shallowRef, markRaw } from 'vue'
// ref:Wrap any value
const count = ref(0)
console.log(count) // RefImpl { _value: 0, __v_isRef: true, ... }
// reactive:Items to Be Packaged
const state = reactive({ count: 0 })
console.log(state) // Proxy { count: 0, ... }
// shallowRef: Shallow
const list = shallowRef([1, 2, 3])
console.log(list) // RefImpl { _value: [1, 2, 3] } // Arrays are not reactive Packaging
// markRaw:Does not respond at all
const map = markRaw(new Map())
console.log(map) // Map { ... } // Regular Map,No Proxy
(4) ▶ サンプル:. 失敗の5つの一般的な根本原因
| 障害シナリオ | 根本原因 | 解決策 |
|---|---|---|
| リアクティブオブジェクト全体を置き換える | リアクティブ参照が変更されました | refでラップするか、そのプロパティを変更する |
| 欠落している応答を分解する | 元の値を分解する | toRefs を使用する |
| 配列への直接インデックス代入 | 内部プロパティはプロキシによってインターセプトされます | splice を使用するか、配列全体を置き換えてください |
| 非同期の代入が応答しない | 非同期関数が予定通りに実行されていない | ref/reactive によるラップ |
| ネストされたオブジェクトのプロパティは更新されない | Vue 3 で修正済み(Vue 2 では Vue.set が必要) | 自動更新 |
(5) ▶ サンプル:. プロキシのパフォーマンスベンチマーク
// Performance Testing:10,000 A property
const obj = {}
for (let i = 0; i < 10000; i++) {
obj[`key${i}`] = i
}
console.time('Object.defineProperty')
// ... 10,000 getter/setter
console.timeEnd('Object.defineProperty')
// Usually 50-100ms
console.time('Proxy')
const proxy = new Proxy(obj, { /* ... */ })
console.timeEnd('Proxy')
// Usually 1-5ms(Create only when accessed)
(6) ▶ サンプル:. デバッグの5つの重要なヒント
// 1. watchEffect Printing Dependencies
watchEffect(() => {
console.log('Dependency:', { a: state.a, b: state.b })
})
// 2. Trigger a manual update(forceUpdate)
import { triggerRef } from 'vue'
triggerRef(stateRef)
// 3. Check the responsive status
import { isReactive, isRef, isReadonly, isProxy } from 'vue'
isReactive(obj) // true/false
isRef(refObj) // true/false
// 4. Original Object(Remove Proxy)
import { toRaw } from 'vue'
const raw = toRaw(reactiveObj)
// 5. Performance Labels
import { markRaw } from 'vue'
const fast = markRaw(expensiveObj)
❓ よくある質問
track と trigger はどこで呼び出されますか?track は、リアクティブオブジェクトの get メソッド内で呼び出されます(get が呼び出された際の依存関係を記録するためです)。triggerは、setメソッド内で呼び出されます(setが呼び出された際に、すべての依存関係に通知するため)。shallowRef と ref の違いは何ですか?ref はディープレスポンスです(ネストされたオブジェクトもプロキシとなります)。shallowRefは、.value全体の置換のみを追跡し、内部のプロパティは処理しません。大規模なリスト(10万行以上)に適しています。computed は、依存関係が変更されたことをどのように検知するのですか?computed は、内部でゲッターを effect でラップしています。ゲッターで使用されているリアクティブデータが変更されると、effectがトリガーされ、dirtyフラグがリセットされるため、次回アクセス時に値が再計算されます。watch と watchEffect の内部実装の違いは何ですか?watch はソース(依存関係)を明示的に指定し、コールバックには newVal と oldVal の両方が渡されます。watchEffectはソースを指定せず、関数本体内の依存関係を自動的に追跡し、コールバックには新しい値のみが渡されます。shallowRef を使用して内部プロパティを変更する際、更新をどのようにトリガーしますか?triggerRef(shallowRef) を使用して手動でトリガーします。シナリオ:内部プロパティを一括変更した後、ビューの再読み込みを強制する場合。reactive でラップされたプロキシは、コンソールに何を表示しますか?Proxy { ... } wrapper object; the properties of the original object are not visible. Use toRaw(proxyObj) が表示されます。📖 まとめ
- Vue 3のリアクティブコア:プロキシのインターセプト + track(依存関係の収集) + trigger(更新のトリガー)
- Proxyは
Object.definePropertyよりも高性能です。配列、Map、Setをネイティブにサポートしており、新しいプロパティが追加されると自動的に反応します。 - 内部参照:RefImpl クラス;get/set 操作中の追跡/トリガー
computedの内部:ダーティフラグ + 遅延評価 + キャッシュ- 5つのパフォーマンス最適化:shallowRef / shallowReactive / markRaw
- レスポンシブ処理が失敗する5つの一般的な原因:置換/構造分解/配列/非同期処理/ネスト
📝 練習問題
(1) 基礎問題(難易度:⭐)
(プロキシに基づく)リアクティブシステムの簡略版を実装する:
- 5行のリアクティブ関数
- 5ラインのトラック+トリガー
- デモ:100回設定 → エフェクトを1回発動(バッチ処理)
(2) 上級問題(難易度:⭐⭐)
shallowRef および markRaw を使用した大規模リストの最適化:
- 1,000件の製品リスト(shallowRefを使用)
- サードパーティ製ライブラリのインスタンスが含まれています(
markRawを使用) - メモリ使用量とFPSの向上を測定する
(3) チャレンジ問題(難易度:⭐⭐⭐)
完全な「依存関係トレーサー」を実装する:
- watchEffect はすべての依存関係を出力します
- triggerRef:更新を手動で実行する
- toRaw、isReactive、isRef 各ユーティリティ関数の実演
- 5つの障害シナリオ(リファレンスの置換/分解/配列/非同期/ネスト)の根本原因分析
- パフォーマンスベンチマーク:ref、shallowRef、markRaw のメモリ比較



