404 Not Found

404 Not Found


nginx

Vue 3 のオブザーバー:`watch` と `watchEffect` の詳細な比較

リスナー(watch / watchEffect)を使用すると、リアクティブデータの変更を監視し、その変更が発生した際に副作用を実行することができます。computedは「新しい値を導出」するために使用され、watchは「アクションをトリガー」するために使用されます(リクエストの発行や状態の設定など)。

watch は Vue のリアクティブシステムの重要な構成要素であり、主に データの変更時に API 呼び出しをトリガーすること、フォーム入力のデバウンス、およびデータ間の同期 という 3 つのユースケースがあります。

1. 学習内容


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サーバーがクラッシュしました:

バックエンド開発者のチャーリーは次のように報告しています:

「アリス、あなたの検索処理が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) 収益

デバウンス処理を実装した後:


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 watchwatchEffect、どちらを使うべきですか?
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) コンポーネントがアンマウントされたとき。タイマーのクリーンアップ、リクエストのキャンセル、イベントリスナーの削除などに適しています。

📖 まとめ


📝 練習問題

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

watch を使用して、ref の変数 count を監視します:

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

watch を使用して「自動保存」機能を実装する:

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

検索、オートコンプリート、履歴のシステムを完全に実装する:

  1. 手ぶれ補正検索(300 ms)
  2. 検索結果の一覧を表示する(v-for)
  3. 直近5件の検索履歴を保存する
  4. 履歴項目をクリックして、再度検索します
  5. 検索語を変更する際、保留中のリクエストをキャンセルする
  6. watchEffect および onCleanup を使用して実装する
Web-Tutorial.com

Web-Tutorial 技術チーム

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

100%