Vue 3 のオブザーバー:`watch` と `watchEffect` の詳細な比較
リスナー(watch / watchEffect)を使用すると、リアクティブデータの変更を監視し、その変更が発生した際に副作用を実行することができます。computedは「新しい値を導出」するために使用され、watchは「アクションをトリガー」するために使用されます(リクエストの発行や状態の設定など)。
watch は Vue のリアクティブシステムの重要な構成要素であり、主に データの変更時に API 呼び出しをトリガーすること、フォーム入力のデバウンス、およびデータ間の同期 という 3 つのユースケースがあります。
1. 学習内容
watch()基本的な構文と4つのパラメータ(source / callback / options / deep)- 4種類のデータソース(ref、reactive、computed、およびゲッター関数)について理解する
watchEffect()自動依存関係追跡とwatchの違いimmediate: true即時トリガーオプションdeep: true内部特性の詳細なモニタリング- 手ぶれ防止スロットリングの実装
- クリーンアップの副作用(onCleanup / コンポーネントのアンロード時)
2. 検索ボックスに関するAPIのパフォーマンスの問題
(2) 課題:キーを押すたびにリクエストが送信され、APIがクラッシュしてしまう
アリスは管理画面に商品検索ボックスを実装しました:
JS
// ❌ The "Flip" Version:A request is sent for each character
import { ref } from 'vue'
const searchQuery = ref('')
watch(searchQuery, async (newValue) => {
// A request is sent with every input
const results = await fetch(`/api/search?q=${newValue}`)
searchResults.value = await results.json()
})
APIサーバーがクラッシュしました:
- ユーザーが「iPhone 15」(8文字)と入力 = 8回のAPI呼び出し
- 100人のユーザーが同時に入力 = 800回のAPI呼び出し
- APIのレート制限:100/分 → サーバーがクラッシュ
バックエンド開発者のチャーリーは次のように報告しています:
「アリス、あなたの検索処理がAPIに過度な負荷をかけています。デバウンスが必要です。ユーザーが入力を止めてから300ミリ秒待ってからリクエストを送信してください。」
(2) ウォッチビュー+手ぶれ補正ソリューション
JS
import { ref, watch } from 'vue'
const searchQuery = ref('')
const searchResults = ref([])
// Image Stabilization:300ms A request is only sent if there are no internal changes.
let timeoutId = null
watch(searchQuery, (newValue) => {
if (timeoutId) clearTimeout(timeoutId)
timeoutId = setTimeout(async () => {
const response = await fetch(`/api/search?q=${newValue}`)
searchResults.value = await response.json()
}, 300)
})
(3) 収益
デバウンス処理を実装した後:
- API呼び出し:検索1回あたり8回 → 検索1回あたり1回(-87%)
- サーバー負荷:800 → 100(-87%)
- 応答時間:50ms(キャッシュ)対 500ms(ライブ)
- サーバーの稼働率:99.9%(クラッシュは発生しません)
3. watch()の基本構文
(1) 4つのパラメータ
JS
import { ref, watch } from 'vue'
const count = ref(0)
watch(
// 1. Monitoring Source:Reactive data to be monitored
count,
// 2. Callback function:(New Value, Old value) => {...}
(newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
},
// 3. Options: immediate, deep, flush
{ immediate: true, deep: true }
)
(2) 審判の指示に耳を傾ける
JS
const count = ref(0)
// Monitoring ref
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
})
(3) 複数の参照の監視
JS
const firstName = ref('Alice')
const lastName = ref('Smith')
// Monitor multiple
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`Name: ${newFirst} ${newLast}`)
})
4. 4種類のリスニングソース
(1) 審判の指示に耳を傾ける
JS
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
})
(2) リアクティブオブジェクトのリスニング
JS
import { reactive, watch } from 'vue'
const user = reactive({ name: 'Alice', age: 25 })
// Monitor the entire reactive(Automatic Depth)
watch(user, (newVal, oldVal) => {
console.log('User changed:', newVal)
})
// ✅ Modifying internal properties also triggers
user.name = 'Bob' // Trigger
(3) ゲッター関数のリスニング
JS
const state = reactive({ user: { profile: { name: 'Alice' } } })
// Monitor only specific properties(Precise Control)
watch(
() => state.user.profile.name,
(newVal) => {
console.log(`Name: ${newVal}`)
}
)
(4) computed に耳を澄ます
JS
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
watch(doubleCount, (newVal) => {
console.log(`Double: ${newVal}`)
})
(4) 4種類のモニタリング情報源の比較
| データソース | 表記 | 詳細 | 適用対象 |
|---|---|---|---|
| ref | watch(ref, fn) |
デフォルトのライト | 単一の参照 |
| リアクティブ | watch(reactive, fn) |
自動ディープ | オブジェクト |
| ゲッター | watch(() => obj.x, fn) |
デフォルトの浅い参照 | 特定のプロパティ |
| 計算値 | watch(computed, fn) |
デフォルトの光 | 導出値 |
5. watchEffect(): 自動依存関係追跡
(1) 基本的な構文
JS
import { ref, watchEffect } from 'vue'
const count = ref(0)
const name = ref('Alice')
// You don't need to specify who to listen for.,Reactive data used within the function body is automatically tracked
watchEffect(() => {
console.log(`count: ${count.value}, name: ${name.value}`)
// count Re-execute when changes occur
// name Re-execute when changes occur
})
(2) watchEffect 対 watch
| アスペクト | ウォッチ | ウォッチエフェクト |
|---|---|---|
| 再生ソース | 手動指定 | 自動追跡 |
| 今すぐ実行 | デフォルト:いいえ (immediate: true が必要) |
✅ はい |
| 従来の価値観と新しい価値観 | ✅ 両方 | ❌ 新しい価値観のみ |
| クリーンアップの副作用 | onCleanup | onCleanup |
| ユースケース | 精密なモニタリング | 一般的な副作用 |
(3) watchEffect のクリーンアップ
JS
import { watchEffect, onCleanup } from 'vue'
watchEffect((onCleanup) => {
// Set a Timer, Subscribe, Event Listening
const timer = setInterval(() => {
console.log('tick')
}, 1000)
// Cleanup:Called before the next execution or when the component is unloaded
onCleanup(() => {
clearInterval(timer)
})
})
6. オプション:immediate / deep / flush
(1) 即時:直ちに発動する
JS
const count = ref(0)
// Default:count Change is what triggers it
watch(count, (newVal) => console.log(newVal)) // Will not be executed immediately
// immediate: true: Trigger Immediately 1 time
watch(count, (newVal) => console.log(newVal), { immediate: true })
// ✅ Execute Now:0 → 0
(2) deep: 詳細な監視
JS
import { ref, watch } from 'vue'
const user = ref({ profile: { name: 'Alice' } })
// Default:ref The object being wrapped .value It is triggered only when the entire replacement is made
watch(user, () => console.log('changed')) // ❌ user.value.profile.name = 'Bob' Does not trigger
// deep: true:Changes to internal properties also trigger
watch(user, () => console.log('changed'), { deep: true })
// ✅ user.value.profile.name = 'Bob' Trigger
(3) flush:トリガーされたとき
JS
// Default 'pre':Triggered before the component is updated
watch(source, fn, { flush: 'pre' })
// 'post':Triggered after a component update(DOM Updated)
watch(source, fn, { flush: 'post' })
// 'sync':Synchronous Triggering(Not commonly used,Poor performance)
watch(source, fn, { flush: 'sync' })
7. 完全な例:検索ボックス + デバウンス
(1) ▶ サンプル:. レフェリー用のベーシックな腕時計
VUE
<template>
<div>
<input v-model="count">
<p>Count: {{ count }}</p>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`Count changed: ${oldVal} → ${newVal}`)
})
</script>
(2) ▶ サンプル:. 検索のデバウンス
VUE
<template>
<div>
<input v-model="searchQuery" placeholder="Search products...">
<p>Results: {{ results.length }} items</p>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const searchQuery = ref('')
const results = ref([])
let timeoutId = null
watch(searchQuery, (newQuery) => {
// Clear the previous timer
if (timeoutId) clearTimeout(timeoutId)
// Set a new timer:300ms Send the request afterward
timeoutId = setTimeout(async () => {
if (newQuery.length === 0) {
results.value = []
return
}
const res = await fetch(`/api/search?q=${newQuery}`)
results.value = await res.json()
}, 300)
})
</script>
(3) ▶ サンプル:. 反応の深さの変化に注意を払う
JS
import { reactive, watch } from 'vue'
const user = reactive({
profile: { name: 'Alice', age: 25 },
settings: { theme: 'dark' }
})
// Monitor the entire user(Automatic Depth)
watch(user, (newVal) => {
console.log('User changed:', JSON.stringify(newVal))
})
// ✅ Modifying any property triggers
user.profile.name = 'Bob' // Trigger
user.settings.theme = 'light' // Trigger
(4) ▶ サンプル:. watchEffect による自動追跡
JS
import { ref, watchEffect } from 'vue'
const count = ref(0)
const name = ref('Alice')
// Do not specify a source to listen to,Automatic tracking used in the function body
watchEffect(() => {
console.log(`count: ${count.value}, name: ${name.value}`)
// count Re-execute when changes occur
// name Re-execute when changes occur
})
// Execute Now 1 time
count.value = 1 // Re-execute
name.value = 'Bob' // Re-execute
(5) ▶ サンプル:. デバウンスとスロットリングの実装
JS
import { ref, watch } from 'vue'
const searchQuery = ref('')
// Image Stabilization:300ms Last trigger inside
let debounceId = null
watch(searchQuery, (val) => {
if (debounceId) clearTimeout(debounceId)
debounceId = setTimeout(() => {
console.log('Search:', val)
}, 300)
})
// Cost-Cutting: Max 1 trigger per second
const scrollY = ref(0)
let lastRun = 0
watch(scrollY, (val) => {
const now = Date.now()
if (now - lastRun >= 1000) {
lastRun = now
console.log('Scroll:', val)
}
})
(6) ▶ サンプル:. よくある5つの間違いに関するクイックリファレンス
| エラー | 症状 | 解決策 |
|---|---|---|
| 「deep」を設定せずにリアクティブを監視する | 内部プロパティの変更はトリガーされない | watch(obj, fn, { deep: true }) |
| refラッパーオブジェクト全体を監視する | 内部プロパティの変更ではトリガーされない | リアクティブに切り替えるか、「deep」を追加する |
| watchEffect が無限ループを引き起こす | ブラウザがフリーズする | 関数内で使用されるデータの変更は避ける |
| タイマーのクリーンアップが行われていない | メモリリーク | onCleanup を使用する |
| 直ちに難読化を実行 | 以前の値を使いたいが取得できなかった | immediate + oldVal を使用(未定義の場合あり) |
❓ よくある質問
Q
watch と watchEffect、どちらを使うべきですか?A 可能な限り
watch を使用してください(正確な制御や、新旧の値へのアクセスが可能になります)。watchEffectは、特定のデータが変更されたときにアクションを実行したいが、すべての依存関係を一覧表示したくない場合に適しています。パフォーマンス面では、これらはおおむね同等ですが、watchEffectは(関数本体内で依存関係を追跡する必要があるため)わずかに遅くなります。Q
ref を監視する場合、旧値と新値の型は何ですか?A
.value です。ref がオブジェクトをラップしている場合、旧値と新値の両方が ref.value になります(値全体が置き換えられない限り、参照は同じです)。内部プロパティの変更を監視するには、reactive またはゲッター関数を使用してください。Q
watch を使用して ref ラッパーオブジェクトをオブザーブする場合、その内部プロパティをどのようにオブザーブすればよいですか?A 3つの方法があります:(1)
watch(ref, fn, { deep: true });(2) watch(() => ref.value.x, fn)(ゲッター);(3) リアクティブラッパーに切り替える。Q
watch はプロパティを監視できますか?A はい。
watch(() => props.count, fn)。なお、プロパティは読み取り専用ですので、watch コールバック内で変更しないでください。Q
immediate: true のとき、oldVal とは何ですか?A
undefined です(最初のトリガー時には「以前の値」は存在しません)。以前の値は、リファレンスの初期値またはストアから取得できます。Q
watch は非同期に実行できますか?A コールバック関数は
async として実装できますが、watch 自体は同期的にトリガーされます。非同期ロジックはコールバック関数内に記述してください:(newVal) => { setTimeout(() => {...}, 0) }。Q
onCleanup はいつトリガーされますか?A 以下の 2 つのタイミングでトリガーされます:(1) ウォッチが再実行される前(最後のクリーンアップの後)、(2) コンポーネントがアンマウントされたとき。タイマーのクリーンアップ、リクエストのキャンセル、イベントリスナーの削除などに適しています。
📖 まとめ
- watch:リアクティブデータの変更を監視します。パラメータは4つ:source / callback / options / deep
- 4種類のウォッチャー:ref / reactive / ゲッター関数 / computed
- watchEffect は、関数本体内のリアクティブな依存関係を自動的に追跡し、その関数を一度だけ即座に実行します
- immediate: true — 即座にトリガーされる;deep: true — 内部プロパティを詳細に監視する
- スタッター防止:前回のトリガーから300ミリ秒以内に発生した場合にのみ実行される;スロットリング:1秒に1回まで
- onCleanup: 副作用のクリーンアップ:タイマー、サブスクリプション、イベントリスナー
- watch と watchEffect:watch はきめ細かな制御が可能で、新旧両方の値を追跡します。一方、watchEffect は変更を自動的に追跡するため、よりシンプルです。
📝 練習問題
(1) 基礎問題(難易度:⭐)
watch を使用して、ref の変数 count を監視します:
- カウントが変更されたら、「count: ${old} → ${new}」と表示する
immediate: trueを使用すると、コールバックを一度だけ即座に実行します
(2) 上級問題(難易度:⭐⭐)
watch を使用して「自動保存」機能を実装する:
- 参照テキストを編集
content - 1秒後に自動的に
save(content)関数を呼び出します(デバウンス) - 別のページに切り替える際(コンポーネントのアンロード時)にタイマーをリセットする
(3) チャレンジ問題(難易度:⭐⭐⭐)
検索、オートコンプリート、履歴のシステムを完全に実装する:
- 手ぶれ補正検索(300 ms)
- 検索結果の一覧を表示する(v-for)
- 直近5件の検索履歴を保存する
- 履歴項目をクリックして、再度検索します
- 検索語を変更する際、保留中のリクエストをキャンセルする
watchEffectおよびonCleanupを使用して実装する



