本文へジャンプ

ウォッチャー

基本の例

算出プロパティを使うと、派生した値を宣言的に算出することができるようになります。しかしながら、状態の変更に応じて「副作用」を実行する必要とする場合があります。たとえば、DOM が変化する、あるいは非同期処理の結果に基づいて、別の状態に変更した場合といったものです。

Options API では、watch オプション を使って、リアクティブなプロパティが変更されるたびに関数を実行することができます:

js
export default {
  data() {
    return {
      question: '',
      answer: 'Questions usually contain a question mark. ;-)',
      loading: false
    }
  },
  watch: {
    // 問題内容が変更されるたびに、関数が実行されます。
    question(newQuestion, oldQuestion) {
      if (newQuestion.includes('?')) {
        this.getAnswer()
      }
    }
  },
  methods: {
    async getAnswer() {
      this.loading = true
      this.answer = 'Thinking...'
      try {
        const res = await fetch('https://yesno.wtf/api')
        this.answer = (await res.json()).answer
      } catch (error) {
        this.answer = 'Error! Could not reach the API. ' + error
      } finally {
        this.loading = false
      }
    }
  }
}
template
<p>
  Ask a yes/no question:
  <input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>

Playground で試す

watch オプションはドットで区切られたパスをキーとして使うこともできます。

js
export default {
  watch: {
    // 注意 単純なパスのみ対応しています。式は対応していません。
    'some.nested.key'(newValue) {
      // ...
    }
  }
}

Composition API では、watch 関数 を使用することでリアクティブな状態の一部が変更されるたびにコールバックを実行することができます:

vue
<script setup>
import { ref, watch } from 'vue'

const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)

// watch 関数は ref を直接扱えます
watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.includes('?')) {
    loading.value = true
    answer.value = 'Thinking...'
    try {
      const res = await fetch('https://yesno.wtf/api')
      answer.value = (await res.json()).answer
    } catch (error) {
      answer.value = 'Error! Could not reach the API. ' + error
    } finally {
      loading.value = false
    }
  }
})
</script>

<template>
  <p>
    Ask a yes/no question:
    <input v-model="question" :disabled="loading" />
  </p>
  <p>{{ answer }}</p>
</template>

Playground で試す

監視ソースの種類

watch の第一引数は、リアクティブな「ソース」のさまざまな型に対応しています: その引数には ref(算出 ref も含む)やリアクティブなオブジェクト、getter 関数、あるいは複数のソースの配列といったものです:

js
const x = ref(0)
const y = ref(0)

// 単一の ref
watch(x, (newX) => {
  console.log(`x is ${newX}`)
})

// getter
watch(
  () => x.value + y.value,
  (sum) => {
    console.log(`sum of x + y is: ${sum}`)
  }
)

// 複数のソースの配列
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x is ${newX} and y is ${newY}`)
})

以下のようなリアクティブのオブジェクトのプロパティを監視できないことに注意してください:

js
const obj = reactive({ count: 0 })

// これは、watch() に数値を渡しているので動作しません。
watch(obj.count, (count) => {
  console.log(`Count is: ${count}`)
})

代わりに、getter を使います:

js
// 代わりに、getter を使います:
watch(
  () => obj.count,
  (count) => {
    console.log(`Count is: ${count}`)
  }
)

ディープ・ウォッチャー

watch はデフォルトではネストが浅い場合にしか対応していません: そのため、コールバックは監視対象のプロパティに新しい値が割り当てられた場合にしか実行されません。- そのため、ネストしたプロパティの変更があった場合には実行されません。もし、ネストしたすべての変更でコールバックが実行されるようにする場合、ディープ・ウォッチャーを使用する必要があります。

js
export default {
  watch: {
    someObject: {
      handler(newValue, oldValue) {
        // 注意: オブジェクト自体が置き替わらない限り、
        // ネストした変更では、 `newValue` は、`oldValue` と
        // 等しくなります。
      },
      deep: true
    }
  }
}

リアクティブなオブジェクト上で、watch() 関数を直接呼び出すとき、暗黙的にディープ・ウォッチャーが生成されます。 - そのため、コールバックはすべてのネストした変更で実行されます:

js
const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // ネストしたプロパティの変更で実行されます
  // 注意: `newValue` は、`oldValue` と同じだとみなされます。
  // なぜなら、両者はともに同じオブジェクトを示しているからです。
})

obj.count++

これは、リアクティブなオブジェクトを返す getter により、差別化されます。 - 後者の場合、異なるオブジェクトを返したときにのみコールバックは実行されます。

js
watch(
  () => state.someObject,
  () => {
    // state.someObject が置き換わった時のみ実行されます。
  }
)

しかしながら、deep オプションを明示的に使用すれば、続いての例で強制的にディープ・ウォッチャーにすることができます。

js
watch(
  () => state.someObject,
  (newValue, oldValue) => {
    //注意: `newValue` は、`oldValue` と同じだとみなされます。
    //state.someObject が置き替わらない*限りは*。
  },
  { deep: true }
)

Vue 3.5+ では、deep オプションは、最大探索深度を示す数値、つまり、Vue がオブジェクトのネストしたプロパティを何階層まで探索するかを示す数値も指定可能です。

使用上の注意

deep watch は、監視対象のオブジェクトのネストされた全てのプロパティをトラバースする必要があるため、大きなデータ構造で使用するときにはコストが高くなります。使用するときは、どうしても必要なときにだけ使用し、パフォーマンスへの影響に注意しましょう。

即時ウォッチャー

watch は、デフォルトでは、遅延して実行されます: 監視対象の値が変更するまでコールバックは実行されません。しかし、同様のコールバックのロジックを先に実行したい場合もあります。- たとえば、初期値のデータを読み込み、関連する状態が変更されるたび、再びデータを読み込みたいときです。

handler 関数と immediate: true オプションを設定したオブジェクトを利用して宣言することで、監視対象のコールバック関数をすぐ実行させることができます:

js
export default {
  // ...
  watch: {
    question: {
      handler(newQuestion) {
        // コンポーネントが生成されるとすぐに実行されます。
      },
      // 前倒しして、コールバックの実行を強制します。
      immediate: true
    }
  }
  // ...
}

ハンドラー関数の初回実行は、created フックの直前で行われます。Vue は datacomputedmethods オプションを処理済みなので、これらのプロパティは初回呼び出しの際に利用可能です。

immediate: true オプションを渡すと、ウォッチャーのコールバックを強制的に即時実行させられます:

js
watch(
  source,
  (newValue, oldValue) => {
    // すぐに実行され、`source` が変更されると再び実行される
  },
  { immediate: true }
)

一度きりのウォッチャー

  • 3.4 以上でのみサポートされています

ウォッチャーのコールバックは、監視対象のソースが変更されるたびに実行されます。ソースが変更されたときに一度だけコールバックを起動したい場合は、once: true オプションを使用します。

js
export default {
  watch: {
    source: {
      handler(newValue, oldValue) {
        // `source` が変更された時、一度だけトリガーされる
      },
      once: true
    }
  }
}
js
watch(
  source,
  (newValue, oldValue) => {
    // `source` が変更された時、一度だけトリガーされる
  },
  { once: true }
)

watchEffect()

ウォッチャーのコールバックは、ソースと全く同じリアクティブな状態を使用するのが一般的です。例えば、次のコードでは todoId の ref が変更されるたびに、ウォッチャーを使ってリモートのリソースを読み込んでいます:

js
const todoId = ref(1)
const data = ref(null)

watch(
  todoId,
  async () => {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
    )
    data.value = await response.json()
  },
  { immediate: true }
)

特に、ウォッチャーが todoId を 2 回使用していることに注目してください。1 回目はソースとして、2 回目はコールバック内で使用しています。

これは、watchEffect() によって簡略化できます。watchEffect() によって、コールバックのリアクティブな依存関係を自動的に追跡できます。上記のウォッチャーは次のように書き換えられます:

js
watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
})

コールバックは即時実行されるので、immediate: true を指定する必要はありません。実行中は、自動的に todoId.value を依存関係として追跡します(算出プロパティと同様)。todoId.value が変更されるたびに、コールバックが再実行されます。watchEffect() を使用すると、ソース値として明示的に todoId を渡す必要がなくなります。

この例では、watchEffect と実際にデータの読み込みがリアクティブに行われている様子をチェックすることができます。

このような、依存関係が 1 つしかない例では、watchEffect() のメリットは比較的小さいです。しかし、複数の依存関係があるウォッチャーでは、watchEffect() を使うことで、依存関係のリストを手動で管理する負担がなくなります。さらに、ネストしたデータ構造内の複数のプロパティを監視する必要がある場合、watchEffect() は、すべてのプロパティを再帰的に追跡するのではなく、コールバックで使用されるプロパティのみを追跡するので、ディープ・ウォッチャーよりも効率的であることがわかるでしょう。

TIP

watchEffect同期的な処理中のみ依存先を追跡します。非同期のコールバックで使用する場合、最初の await の前にアクセスされたプロパティのみが追跡されます。

watchwatchEffect

watchwatchEffect は、どちらとも副作用をリアクティブに処理することができます。両者の主な違いはリアクティブの依存先の監視方法にあります:

  • watch は明示的に示されたソースしか監視しません。コールバック内でアクセスされたものは追跡しません。加えて、コールバックはソースが実際に変更したときにのみ実行されます。watch は依存関係の追跡を副作用から分離します。それにより、コールバックをいつ実行するかをより正確にコントロールすることができます。

  • それに対して、watchEffect は依存先の追跡と副作用を 1 つのフェーズにまとめたものになります。同期処理を実行している間にアクセスしたすべてのリアクティブのプロパティを自動的に追跡します。これにより、より便利で一般的にコードが短くなりますが、そのリアクティブの依存先はあまり明示的にならなくなってしまいます。

副作用のクリーンアップ

ウォッチャー内で、例えば非同期リクエストなど、副作用が伴う場合があります:

js
watch(id, (newId) => {
  fetch(`/api/${newId}`).then(() => {
    // コールバックのロジック
  })
})
js
export default {
  watch: {
    id(newId) {
      fetch(`/api/${newId}`).then(() => {
        // コールバックのロジック
      })
    }
  }
}

しかし、リクエストが完了する前に id が変更されたらどうなるでしょう?前のリクエストが完了したときに、すでに古くなった ID 値でコールバックが発火してしまいます。理想的には、id が新しい値に変更されたときに古いリクエストをキャンセルしたいです。

onWatcherCleanup() API を使って、ウォッチャーが無効になり再実行される直前に呼び出されるクリーンアップ関数を登録することができます:

js
import { watch, onWatcherCleanup } from 'vue'

watch(id, (newId) => {
  const controller = new AbortController()

  fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
    // コールバックのロジック
  })

  onWatcherCleanup(() => {
    // 古くなったリクエストを中止する
    controller.abort()
  })
})
js
import { onWatcherCleanup } from 'vue'

export default {
  watch: {
    id(newId) {
      const controller = new AbortController()

      fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
        // コールバックのロジック
      })

      onWatcherCleanup(() => {
        // 古くなったリクエストを中止する
        controller.abort()
      })
    }
  }
}

onWatcherCleanup は Vue 3.5+ でのみサポートされており、watchEffect エフェクト関数または watch コールバック関数の同期実行中に呼び出す必要があることに注意してください: 非同期関数の await ステートメントの後に呼び出すことはできません。

代わりに、onCleanup 関数も第 3 引数としてウォッチャーコールバックに渡されwatchEffect エフェクト関数には第 1 引数として渡されます:

js
watch(id, (newId, oldId, onCleanup) => {
  // ...
  onCleanup(() => {
    // クリーンアップのロジック
  })
})

watchEffect((onCleanup) => {
  // ...
  onCleanup(() => {
    // クリーンアップのロジック
  })
})
js
export default {
  watch: {
    id(newId, oldId, onCleanup) {
      // ...
      onCleanup(() => {
        // クリーンアップのロジック
      })
    }
  }
}

これは 3.5 以前のバージョンで動作します。また、関数の引数で渡される onCleanup はウォッチャーインスタンスにバインドされるため、onWatcherCleanup の同期的な制約は受けません。

コールバックが実行されるタイミング

リアクティブな状態が変更されるとき、Vue コンポーネントの更新と生成されたウォッチャーコールバックを実行します。

コンポーネントの更新と同様に、ユーザーが作成したウォッチャーのコールバックは、重複実行を避けるためにバッチ処理されます。例えば、監視対象の配列に 1000 個のアイテムを同期的にプッシュする場合、ウォッチャーが 1000 回起動するのはおそらく望ましくないでしょう。

デフォルトでは、ウォッチャーのコールバックは親コンポーネント(存在する場合)の更新、オーナーコンポーネントの DOM 更新に呼び出されます。つまり、ウォッチャーのコールバック内でオーナーコンポーネント自身の DOM にアクセスしようとすると、DOM は更新前の状態になります。

遅延ウォッチャー

ウォッチャーコールバック内で、Vue が更新したのオーナーコンポーネントの DOM にアクセスしたい場合は、flush: 'post' オプションを指定する必要があります:

js
export default {
  // ...
  watch: {
    key: {
      handler() {},
      flush: 'post'
    }
  }
}
js
watch(source, callback, {
  flush: 'post'
})

watchEffect(callback, {
  flush: 'post'
})

フラッシュ後の watchEffect() には、便利なエイリアスである watchPostEffect() があります:

js
import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* Vue 更新後に実行されます*/
})

同期ウォッチャー

Vue が管理する更新の前に、同期的に起動するウォッチャーを作成することもできます:

js
export default {
  // ...
  watch: {
    key: {
      handler() {},
      flush: 'sync'
    }
  }
}
js
watch(source, callback, {
  flush: 'sync'
})

watchEffect(callback, {
  flush: 'sync'
})

sync の watchEffect() には、便利なエイリアスの watchSyncEffect() もあります:

js
import { watchSyncEffect } from 'vue'

watchSyncEffect(() => {
  /* リアクティブなデータ変更時に同期的に実行される */
})

使用には注意が必要

同期ウォッチャーにはバッチ機能はなく、リアクティブな変更が検出されるたびにトリガーされます。単純な真偽値を監視するために使うのは構いませんが、配列のように何度も同期的に変更される可能性のあるデータソースでは使わないようにしましょう。

this.$watch()

また、$watch() インスタンスメソッド を使用してウォッチャーを強制的に作成することが可能です:

js
export default {
  created() {
    this.$watch('question', (newQuestion) => {
      // ...
    })
  }
}

これは、条件付きでウォッチャーをセットアップする必要があるときや、ユーザーの相互作用に応じる場合にのみ、何かを監視しないといけないときに役立ちます。これにより、ウォッチャーを早い段階で停止することができます。

ウォッチャーの停止

watch オプションを使って宣言したウォッチャー、あるいは $watch() インスタンスメソッドは、オーナーコンポーネントがアンマウントされた自動的に停止します。そのため、多くの場合において、ウォッチャー自体が停止することを心配する必要はありません。

ごくまれに、オーナーコンポーネントがアンマウントされる前に停止する必要がある場合には、$watch() API は次のような関数を返します:

js
const unwatch = this.$watch('foo', callback)

// ...wathcer が必要なくなったとき:
unwatch()

setup(), あるいは <script setup> 内に同期的に宣言されたウォッチャーにより、オーナーコンポーネントのインスタンスにバインドされます。それにより、オーナーコンポーネントがアンマウントされたときに自動的に停止されます。多くの場合、ウォッチャーを自分で停止させることを心配する必要はありません。

ここで重要なのは、ウォッチャーが同期的に 生成されなければならないということです。もしウォッチャーが非同期なコールバックで生成されたら、オーナーコンポーネントにはバインドされず、メモリーリークを避けるために手動で停止しなければいけないからです。それは次の通りです:

vue
<script setup>
import { watchEffect } from 'vue'

// これは自動的に停止します
watchEffect(() => {})

// ...これは自動では停止しません
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

手動でウォッチャーを停止するには、返されたハンドル関数を使用します。これは、watchwatchEffect のどちらにも作用します:

js
const unwatch = watchEffect(() => {})

// ...あとで、もはや必要なくなったとき
unwatch()

非同期的にウォッチャーを作成する必要がある場合はほとんどなく、可能な限り同期的に作成する方が望ましいことに注意してください。もし非同期のデータを待つ必要があるなら、代わりに watch のロジックを条件付きにすることができます:

js
// 非同期的にロードされるデータ
const data = ref(null)

watchEffect(() => {
  if (data.value) {
    // データがロードされたときに何かを実行します
  }
})
ウォッチャーが読み込まれました