ウォッチャー
基本の例
算出プロパティを使うと、派生した値を宣言的に算出することができるようになります。しかしながら、状態の変更に応じて「副作用」を実行する必要とする場合があります。たとえば、DOM が変化する、あるいは非同期処理の結果に基づいて、別の状態に変更した場合といったものです。
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>
監視ソースの種類
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
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
は、デフォルトでは、遅延して実行されます: 監視対象の値が変更するまでコールバックは実行されません。しかし、同様のコールバックのロジックを先に実行したい場合もあります。- たとえば、初期値のデータを読み込み、関連する状態が変更されるたび、再びデータを読み込みたいときです。
immediate: true
オプションを渡すと、ウォッチャーのコールバックを強制的に即時実行させられます:
js
watch(
source,
(newValue, oldValue) => {
// すぐに実行され、`source` が変更されると再び実行される
},
{ immediate: true }
)
一度きりのウォッチャー
- 3.4 以上でのみサポートされています
ウォッチャーのコールバックは、監視対象のソースが変更されるたびに実行されます。ソースが変更されたときに一度だけコールバックを起動したい場合は、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
の前にアクセスされたプロパティのみが追跡されます。
watch
対 watchEffect
watch
と watchEffect
は、どちらとも副作用をリアクティブに処理することができます。両者の主な違いはリアクティブの依存先の監視方法にあります:
watch
は明示的に示されたソースしか監視しません。コールバック内でアクセスされたものは追跡しません。加えて、コールバックはソースが実際に変更したときにのみ実行されます。watch
は依存関係の追跡を副作用から分離します。それにより、コールバックをいつ実行するかをより正確にコントロールすることができます。それに対して、
watchEffect
は依存先の追跡と副作用を 1 つのフェーズにまとめたものになります。同期処理を実行している間にアクセスしたすべてのリアクティブのプロパティを自動的に追跡します。これにより、より便利で一般的にコードが短くなりますが、そのリアクティブの依存先はあまり明示的にならなくなってしまいます。
副作用のクリーンアップ
ウォッチャー内で、例えば非同期リクエストなど、副作用が伴う場合があります:
js
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()
})
})
onWatcherCleanup
は Vue 3.5+ でのみサポートされており、watchEffect
エフェクト関数または watch
コールバック関数の同期実行中に呼び出す必要があることに注意してください: 非同期関数の await
ステートメントの後に呼び出すことはできません。
代わりに、onCleanup
関数も第 3 引数としてウォッチャーコールバックに渡され、watchEffect
エフェクト関数には第 1 引数として渡されます:
js
watch(id, (newId, oldId, onCleanup) => {
// ...
onCleanup(() => {
// クリーンアップのロジック
})
})
watchEffect((onCleanup) => {
// ...
onCleanup(() => {
// クリーンアップのロジック
})
})
これは 3.5 以前のバージョンで動作します。また、関数の引数で渡される onCleanup
はウォッチャーインスタンスにバインドされるため、onWatcherCleanup
の同期的な制約は受けません。
コールバックが実行されるタイミング
リアクティブな状態が変更されるとき、Vue コンポーネントの更新と生成されたウォッチャーコールバックを実行します。
コンポーネントの更新と同様に、ユーザーが作成したウォッチャーのコールバックは、重複実行を避けるためにバッチ処理されます。例えば、監視対象の配列に 1000 個のアイテムを同期的にプッシュする場合、ウォッチャーが 1000 回起動するのはおそらく望ましくないでしょう。
デフォルトでは、ウォッチャーのコールバックは親コンポーネント(存在する場合)の更新後、オーナーコンポーネントの DOM 更新前に呼び出されます。つまり、ウォッチャーのコールバック内でオーナーコンポーネント自身の DOM にアクセスしようとすると、DOM は更新前の状態になります。
遅延ウォッチャー
ウォッチャーコールバック内で、Vue が更新した後のオーナーコンポーネントの DOM にアクセスしたい場合は、flush: 'post'
オプションを指定する必要があります:
js
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
フラッシュ後の watchEffect()
には、便利なエイリアスである watchPostEffect()
があります:
js
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* Vue 更新後に実行されます*/
})
同期ウォッチャー
Vue が管理する更新の前に、同期的に起動するウォッチャーを作成することもできます:
js
watch(source, callback, {
flush: 'sync'
})
watchEffect(callback, {
flush: 'sync'
})
sync の watchEffect()
には、便利なエイリアスの watchSyncEffect()
もあります:
js
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
/* リアクティブなデータ変更時に同期的に実行される */
})
使用には注意が必要
同期ウォッチャーにはバッチ機能はなく、リアクティブな変更が検出されるたびにトリガーされます。単純な真偽値を監視するために使うのは構いませんが、配列のように何度も同期的に変更される可能性のあるデータソースでは使わないようにしましょう。
ウォッチャーの停止
setup()
, あるいは <script setup>
内に同期的に宣言されたウォッチャーにより、オーナーコンポーネントのインスタンスにバインドされます。それにより、オーナーコンポーネントがアンマウントされたときに自動的に停止されます。多くの場合、ウォッチャーを自分で停止させることを心配する必要はありません。
ここで重要なのは、ウォッチャーが同期的に 生成されなければならないということです。もしウォッチャーが非同期なコールバックで生成されたら、オーナーコンポーネントにはバインドされず、メモリーリークを避けるために手動で停止しなければいけないからです。それは次の通りです:
vue
<script setup>
import { watchEffect } from 'vue'
// これは自動的に停止します
watchEffect(() => {})
// ...これは自動では停止しません
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
手動でウォッチャーを停止するには、返されたハンドル関数を使用します。これは、watch
や watchEffect
のどちらにも作用します:
js
const unwatch = watchEffect(() => {})
// ...あとで、もはや必要なくなったとき
unwatch()
非同期的にウォッチャーを作成する必要がある場合はほとんどなく、可能な限り同期的に作成する方が望ましいことに注意してください。もし非同期のデータを待つ必要があるなら、代わりに watch のロジックを条件付きにすることができます:
js
// 非同期的にロードされるデータ
const data = ref(null)
watchEffect(() => {
if (data.value) {
// データがロードされたときに何かを実行します
}
})