Vue 3 の計算プロパティ:計算結果のキャッシュと依存関係の追跡
計算プロパティ(computed)は、Vue 3における特別なプロパティであり、リアクティブなデータに基づいて自動的に再計算されます。その最大の利点はキャッシュ機能にあります。つまり、依存するデータが変更された場合にのみ再計算され、それ以外の場合は単に以前の値を返します。計算プロパティを理解することは、高性能なVueアプリケーションを作成するための鍵となります。
計算プロパティの中核をなすのは、自動依存関係追跡+結果のキャッシュ+リアクティブ性という3つの主要な機能です。このレッスンでは、その5つのユースケースを習得し、3つの落とし穴を回避する方法を学びます。
1. 学習内容
computed()基本的な構文と戻り値- 計算プロパティとメソッドの主な違い(キャッシュ)
- ゲッター/セッターを持つ双方向の計算プロパティ
- 計算プロパティへの連鎖呼び出し(計算プロパティ内で計算されるもの)
- テンプレートとメソッドにおける「computed」の異なる用法
- 3つのよくある落とし穴(誤用、循環依存、パフォーマンスの問題)
2. ショッピングカートの合計金額に関するパフォーマンスの問題
(1) 課題:クリックするたびに100個の商品の再計算が行われる
アリスは、自身のECサイトの管理画面にカートの合計金額計算機能を構築しました:
// ❌ The "Flip" Version: Use methods
function cartTotal(items) {
console.log('Calculating...') // Print on every call
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
<template>
<div>
<p>Total: ${{ cartTotal(items) }}</p>
<p>Tax: ${{ cartTotal(items) * 0.08 }}</p>
<p>Grand Total: ${{ cartTotal(items) * 1.08 }}</p>
<button @click="showDate = new Date()">Update Time</button>
</div>
</template>
パフォーマンスがひどいです:
- ページの読み込み:計算が1回実行されました ✅
- 1回のレンダリング:3回(合計、税額、総計)=300項目の計算
- ユーザーが「更新時間」をクリック(アイテムとは無関係):変更は0件だが、計算は依然として3回実行される = 300件のアイテム計算
パフォーマンス監視ツールには、次のように表示されています:
「cartTotal関数は、ページ読み込みごとに5,000回呼び出される。100アイテム × 5,000 = 500,000回のアイテムごとの計算。3秒間のカクつき。」
(2) Vueのcomputedによる解決策:キャッシュ + 依存関係の追跡
import { ref, computed } from 'vue'
const items = ref([
{ price: 100, quantity: 2 },
{ price: 50, quantity: 1 }
])
// ✅ computed:Rely solely on items,items If it hasn't changed, don't recalculate it.
const cartTotal = computed(() => {
console.log('Calculating...') // Only at items Print when changes occur
return items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
})
// computed Auto-Processing 3 For use in: Count only 1 time
const tax = computed(() => cartTotal.value * 0.08)
const grandTotal = computed(() => cartTotal.value * 1.08)
(3) 収益
「computed」に切り替えた後:
- 関数呼び出し:5,000 → 100(-98%)
- インタラクティブになるまでの時間:3秒 → 80ミリ秒(-97%)
- スクロール時のカクつき:5/s → 0 (-100%)
- CPU使用率:80% → 5% (-94%)
3. computed() の基本的な使い方
(1) 標準構文
import { ref, computed } from 'vue'
const count = ref(0)
// Basic Computational Properties
const doubleCount = computed(() => {
return count.value * 2
})
// Abbreviation
const doubleCount = computed(() => count.value * 2)
// Usage
console.log(doubleCount.value) // 0
count.value = 5
console.log(doubleCount.value) // 10
(2) テンプレートでの使用
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="count++">+1</button>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
</script>
テンプレートでは、{{ doubleCount }} は必須ではありません .value(自動的に展開されます)。
(3) 5つの主な特徴
graph TB
subgraph ComputedKey Features
A[Automatic Dependency Tracking]
B[Cache Results]
C[Responsive]
D[Lazy Evaluation]
E[Chainable calls]
end
style A fill:#42b883,color:#fff
style B fill:#42b883,color:#fff
style C fill:#42b883,color:#fff
style D fill:#42b883,color:#fff
style E fill:#42b883,color:#fff
| 機能 | 説明 |
|---|---|
| 依存関係の自動追跡 | 関数本体内で使用されているリアクティブデータを自動的に検出 |
| キャッシュされた結果 | 依存関係に変更がない場合、複数回のアクセスは1回としてカウントされる |
| レスポンシブ | 依存関係が変更された際に自動的に再計算される |
| 遅延評価 | アクセスされない限り評価されない(パフォーマンスの向上につながる) |
| 連鎖可能 | 計算結果は他の計算済みプロパティに依存する場合がある |
4. 計算式とメソッド:5つの主な違い
| 側面 | 計算値 | 方法 |
|---|---|---|
| キャッシュ | ✅ 依存関係に変更がない場合は再計算しない | ❌ 呼び出しのたびに再計算する |
| レスポンシブ | ✅ 依存関係を自動的に追跡 | ❌ 追跡しない(手動でパラメータを渡す必要がある) |
| テンプレートへのアクセス | {{ doubleCount }} |
{{ doubleCount() }} |
| パラメータ | ❌ 未対応 | ✅ 任意のパラメータを受け付ける |
| ユースケース | 単純な派生値 | 複雑なロジック、イベント処理 |
(4) 5つのシナリオから選択可能
| シナリオ | 計算結果の使用 | メソッドの使用 |
|---|---|---|
| 表示されている数字を2倍にする | ✅ | ❌ |
| カートの合計金額を計算 | ✅ | ❌ |
| ボタンのクリックを処理する | ❌ | ✅ |
| APIリクエストを送信 | ❌ | ✅ |
| 日付の書式設定(依存関係なし) | ❌ | ✅ |
| フィルタ一覧(データによる) | ✅ | ❌ |
| フォームの検証 | ❌ | ✅ |
5. ゲッター/セッター:双方向の計算プロパティ
(1) デフォルトでは、ゲッターのみが存在します
const firstName = ref('Alice')
const lastName = ref('Smith')
// There is only one getter
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
console.log(fullName.value) // Alice Smith
// fullName.value = 'Bob Lee' // ❌ Warning:Calculated properties are read-only.
(2) ゲッターとセッターの完全な実装
const firstName = ref('Alice')
const lastName = ref('Smith')
const fullName = computed({
// Read:Put it together
get() {
return `${firstName.value} ${lastName.value}`
},
// Write:Unpack
set(newValue) {
const parts = newValue.split(' ')
firstName.value = parts[0]
lastName.value = parts[1] || ''
}
})
// ✅ Read and write access is now available
fullName.value = 'Bob Lee'
console.log(firstName.value) // Bob
console.log(lastName.value) // Lee
(3) テンプレートにおける双方向バインディング
<template>
<div>
<input v-model="fullName">
<p>First: {{ firstName }}</p>
<p>Last: {{ lastName }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('Alice')
const lastName = ref('Smith')
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val) => {
const parts = val.split(' ')
firstName.value = parts[0]
lastName.value = parts[1] || ''
}
})
</script>
6. 計算プロパティへの連鎖呼び出し
(1) ネストされた計算式
const items = ref([
{ price: 100, quantity: 2 },
{ price: 50, quantity: 3 }
])
// First Floor:Total Original Price
const subtotal = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
// Second Floor: Tax (Based on subtotal)
const tax = computed(() => subtotal.value * 0.08)
// Third Floor: Discount (Based on subtotal)
const discount = computed(() => subtotal.value > 200 ? 20 : 0)
// Fourth Floor:Final Total Price
const total = computed(() => subtotal.value + tax.value - discount.value)
console.log(total.value) // 350 * 1.08 - 20 = 358
(2) 連鎖型演算キャッシュ機構
graph TB
A[items change] --> B[subtotal Recalculate]
B --> C[tax Recalculate]
B --> D[discount Recalculate]
C --> E[total Recalculate]
D --> E
style A fill:#42b883
style B fill:#42b883
style E fill:#42b883
チェーンに依存する計算値のみが再計算され、使用されていないものは再計算されません。
7. 完全な例:Eコマースのショッピングカートの合計金額
(1) ▶ サンプル:. 基本的な計算
<template>
<div>
<p>Subtotal: ${{ subtotal }}</p>
<p>Tax (8%): ${{ tax }}</p>
<p>Discount: -${{ discount }}</p>
<p><strong>Total: ${{ total }}</strong></p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const items = ref([
{ name: 'iPhone', price: 999, quantity: 1 },
{ name: 'Case', price: 50, quantity: 2 }
])
const subtotal = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const tax = computed(() => subtotal.value * 0.08)
const discount = computed(() => subtotal.value > 1000 ? 50 : 0)
const total = computed(() => subtotal.value + tax.value - discount.value)
</script>
(2) ▶ サンプル:. ゲッターとセッター
<template>
<div>
<input v-model="fullName" placeholder="First Last">
<p>First: {{ firstName }}</p>
<p>Last: {{ lastName }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('Alice')
const lastName = ref('Smith')
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val) => {
const parts = val.split(' ')
firstName.value = parts[0] || ''
lastName.value = parts[1] || ''
}
})
</script>
(3) ▶ サンプル:. 計算されたチェーンコール
import { ref, computed } from 'vue'
const score = ref(85)
// First Floor:Base Score
const baseScore = computed(() => score.value)
// Second Floor:Bonus Points(Base Score × Difficulty Level)
const difficultyBonus = computed(() => {
if (baseScore.value >= 90) return 10
if (baseScore.value >= 80) return 5
return 0
})
// Third Floor:Level
const grade = computed(() => {
const total = baseScore.value + difficultyBonus.value
if (total >= 95) return 'A+'
if (total >= 90) return 'A'
if (total >= 80) return 'B'
return 'C'
})
(4) ▶ サンプル:. 計算式とメソッドの比較
// ❌ methods:Recalculate on every call
function getTotal() {
console.log('Calculated!') // If used multiple times in the template, it will be printed multiple times.
return items.value.reduce((sum, i) => sum + i.price * i.quantity, 0)
}
// ✅ computed: Count only 1 time
const total = computed(() => {
console.log('Calculated!') // Print Only 1 time
return items.value.reduce((sum, i) => sum + i.price * i.quantity, 0)
})
(5) ▶ サンプル:. 5つの性能比較シナリオ
| シーン | 呼び出されたメソッド数 | 計算された呼び出し数 |
|---|---|---|
| テンプレート内で1回使用 | 1 | 1 |
| テンプレート内で3回使用 | 3 | 1 |
| テンプレート内で10回使用 | 10 | 1 |
| 親コンポーネントが更新されたが、項目は変更されていない | 呼び出し済み | 再計算されていない |
| 依存関係の変更 | 再呼び出し | 再計算 |
(6) ▶ サンプル:. よくある5つの間違い
| エラー | 症状 | 解決策 |
|---|---|---|
| メソッドを計算式に置き換える | 繰り返し使用するとパフォーマンスが低下する | 計算式に切り替える |
computed へのパラメータの指定が不正です |
エラー | computed はパラメータに対応していません。代わりに methods を使用してください |
| セッター:独自に作成 | 無限ループ | セッター:依存関係を修正してください。fullName.value = ... は使用しないでください |
| 循環依存関係 A→B→A | 無限ループ | 依存関係のいずれかを削除する |
| 計算プロパティ内に記述された複雑なロジック | デバッグが困難 | メソッドに抽出する。計算プロパティからそのメソッドを呼び出すようにする |
❓ よくある質問
computed と methods、どちらを使うべきですか?computed を使用してください(キャッシュ機能+自動追跡機能)。methods を使用するのは、以下の状況に限ってください:(1) パラメータを渡す必要がある場合;(2) リアクティブデータに依存しない場合;(3) イベントハンドラ(@click など)の場合。computed はパラメータを受け取ることができますか?computed が受け取れるパラメータは 0 個のみです。パラメータを渡す必要がある場合は、代わりに methods を使用してください:getDouble(n) { return n * 2 }、あるいは computed で関数を返すようにしてください:computed(() => (n) => n * 2)。computed は setup の外部からアクセスできますか?computed は ref オブジェクトを返すため、これを他のコンポーネントや関数に渡すことができます。ただし、リアクティブ性を維持するためには、computed を「派生リファレンス」として使用することをお勧めします。computed と watch の違いは何ですか?computed は「派生値」(データに基づいて新しい値を計算するもの)であり、watch は「ウォッチャー」(データが変更された際に副作用を実行するもの)です。computedは純粋な計算に適しており、watchはリクエストの発行や状態の設定などに適しています。computedはmethodsと比べてどのような点で優れているのですか?methodsは呼び出しのたびに関数を実行しますが、computedは依存関係が変更された場合にのみ実行されます。テンプレート内でcomputedを3回使用しても計算は1回のみ行われますが、methodsを使用すると3回の計算が行われます。fullName のセッターでは、fullName.value = ... ではなく、firstName と lastName を更新する必要があります。computed は非同期にできますか?computed は同期型です。非同期操作を行う場合は、watch または watchEffect を使用してください。「非同期の計算プロパティ」が必要な場合は、data() { return { result: null } } と watch + onMounted を組み合わせて使用してください。📖 まとめ
computedは、リアクティブデータに基づいて自動的に再計算を行う「派生参照」です。この機能には、自動追跡、キャッシュ、リアクティブ性、遅延評価、連鎖性の5つの主要な特徴があります。- テンプレート内の
{{ doubleCount }}には .value を指定する必要はありません(自動アンパックされます)。JS 内のdoubleCount.value - 計算式とメソッドの違い:計算式はキャッシュされる(パフォーマンス向上)、メソッドはキャッシュされないがパラメータを受け付ける
- ゲッター/セッターによる双方向の計算プロパティ:読み取りおよび書き込みが可能。セッターは依存関係を更新するが、自身は更新しない。
- 計算プロパティにおける連鎖呼び出し:計算プロパティは他の計算プロパティに依存する場合があります。未使用の計算プロパティは再計算されません。
- 3つの落とし穴:循環依存、メソッドの誤用、デバッグが困難な複雑なロジック
- パフォーマンス:テンプレート内では
computedプロパティが 3 箇所で使用されていますが、評価されるのは 1 回のみであるのに対し、methodsは 3 回評価されます。
📝 練習問題
(1) 基礎問題(難易度:⭐)
テキスト内の単語数を数える計算プロパティ wordCount を作成してください(テキストをスペースで分割するには split を使用します)。
データ:const text = ref('Hello Vue 3 from Alice') 出力結果は 4 になるはずです。
(2) 上級問題(難易度:⭐⭐)
3つの計算プロパティを含む、ショッピングカートの合計金額の計算式を作成してください:
subtotal: すべての商品の価格と数量の合計discount: 100以上のご注文で10割引、200以上のご注文で30割引total:小計 - 割引額- 3点:iPhone 999ドル × 1、ケース 50ドル × 2、充電器 30ドル × 1
(3) チャレンジ問題(難易度:⭐⭐⭐)
完全な「ショッピングカートの合計金額システム」を実装する:
- 5つの商品(それぞれに商品名、価格、数量が記載されている)
- 小計/税(8%)/割引(200以上の購入で20割引)/合計—4つの計算項目
fullName計算プロパティにはゲッターとセッターを使用します。セッター内では、入力値に基づいてfirstNameおよびlastNameを更新します。- 5つの計算プロパティについて、依存関係の追跡をテストする(どのプロパティが、いずれかの製品を変更した後に再計算されるか)
- Mermaid を使用して、計算プロパティの依存関係グラフを描く



