404 Not Found

404 Not Found


nginx

Vue 3 のリアクティブ性の原理を徹底解説:プロキシのインターセプト + 依存関係の収集 + 更新のトリガー

Vue 3 のリアクティビティの仕組みを理解することで、より効率的なコードを記述したり、厄介なリアクティビティの問題をデバッグしたりできるようになります。このレッスンでは、Vue 3 のリアクティビティシステムの基盤となる実装、すなわちプロキシインターセプター、依存関係の解決、更新トリガー、そして ref、計算プロパティ、ウォッチの内部メカニズムについて詳しく解説します。

Vue 3では、Vue 2のObject.definePropertyがES6のProxyに置き換えられ、配列、Map、Setに対するネイティブサポートが提供されるとともに、パフォーマンスが2倍に向上しています。その仕組みを理解すれば、リアクティブデータに関連する5つのよくある落とし穴を回避できるようになります。

1. 学習内容


2. 10万行のテーブルで発生した「フリーズ」事象

(1) 課題:10万件のアイテムのリストをrefで囲むと、ページがフリーズしてしまう

アリスは、管理者用に10,000行の製品テーブルを読み込みました:

JS
// ❌ 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 を使った解決策

JS
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) 収益

最適化後:


3. プロキシのインターセプト:Vue 3 のリアクティブ性の要

(1) プロキシとは何ですか?

プロキシは、ES6で導入されたオブジェクトラッパーであり、13種類のオブジェクト操作(get、set、has、deletePropertyなど)をインターセプトすることができます。

JS
// 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のリアクティブ実装(簡略版)

JS
// 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) フローチャートの全体像

100%
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) グローバル依存関係ストレージの構造

JS
// 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 のソースコード(簡略版)

JS
// 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) 自動展開の仕組み

VUE
<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>

原則:テンプレートのコンパイル時、コンパイラは自動的に .valueref に追加します(手動で記述する必要はありません)。


6. computedの内部実装

(1) 怠惰+不潔フラグ

JS
// 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) キャッシュの仕組み

JS
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: 浅い応答

JS
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: 浅いリアクティブ

JS
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:まったく反応しない

JS
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) ▶ サンプル:. 「トラック+トリガー」の簡略版

JS
// 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 の内部構造

JS
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) ▶ サンプル:. プロキシのパフォーマンスベンチマーク

JS
// 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つの重要なヒント

JS
// 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)
▶ 試してみよう

❓ よくある質問

Q なぜ Vue 3 では Object.defineProperty ではなく Proxy が使われているのですか?
A Proxy は配列、Map、Set をネイティブでサポートしており、より多くの操作をインターセプトでき(13 種類対 2 種類)、新しいプロパティの追加に自動的に対応し、パフォーマンスも優れています(遅延プロキシ)。唯一の欠点は、IE11 をサポートしていないことです。
Q tracktrigger はどこで呼び出されますか?
A track は、リアクティブオブジェクトの get メソッド内で呼び出されます(get が呼び出された際の依存関係を記録するためです)。triggerは、setメソッド内で呼び出されます(setが呼び出された際に、すべての依存関係に通知するため)。
Q shallowRefref の違いは何ですか?
A ref はディープレスポンスです(ネストされたオブジェクトもプロキシとなります)。shallowRefは、.value全体の置換のみを追跡し、内部のプロパティは処理しません。大規模なリスト(10万行以上)に適しています。
Q computed は、依存関係が変更されたことをどのように検知するのですか?
A computed は、内部でゲッターを effect でラップしています。ゲッターで使用されているリアクティブデータが変更されると、effectがトリガーされ、dirtyフラグがリセットされるため、次回アクセス時に値が再計算されます。
Q watchwatchEffect の内部実装の違いは何ですか?
A watch はソース(依存関係)を明示的に指定し、コールバックには newValoldVal の両方が渡されます。watchEffectはソースを指定せず、関数本体内の依存関係を自動的に追跡し、コールバックには新しい値のみが渡されます。
Q shallowRef を使用して内部プロパティを変更する際、更新をどのようにトリガーしますか?
A triggerRef(shallowRef) を使用して手動でトリガーします。シナリオ:内部プロパティを一括変更した後、ビューの再読み込みを強制する場合。
Q reactive でラップされたプロキシは、コンソールに何を表示しますか?
A 元のオブジェクトを取得するために、Proxy { ... } wrapper object; the properties of the original object are not visible. Use toRaw(proxyObj) が表示されます。

📖 まとめ


📝 練習問題

(1) 基礎問題(難易度:⭐)

(プロキシに基づく)リアクティブシステムの簡略版を実装する:

(2) 上級問題(難易度:⭐⭐)

shallowRef および markRaw を使用した大規模リストの最適化:

(3) チャレンジ問題(難易度:⭐⭐⭐)

完全な「依存関係トレーサー」を実装する:

  1. watchEffect はすべての依存関係を出力します
  2. triggerRef:更新を手動で実行する
  3. toRaw、isReactive、isRef 各ユーティリティ関数の実演
  4. 5つの障害シナリオ(リファレンスの置換/分解/配列/非同期/ネスト)の根本原因分析
  5. パフォーマンスベンチマーク:ref、shallowRef、markRaw のメモリ比較
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%