Watchers
基本の例
算出プロパティを使うと、派生した値を宣言的に算出することができるようになります。しかしながら、状態の変更に応じて「副作用」を実行する必要とする場合があります。たとえば、DOM が変化する、あるいは非同期処理の結果に基づいて、別の状態にに変更した場合といったものです。
Composition API では、watch
関数 を使用することでリアクティブな状態の一部が変更されるたびにコールバックが実行することができます:
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
// watch 関数は ref を直接扱えます
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.indexOf('?') > -1) {
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
}
}
})
</script>
<template>
<p>
Ask a yes/no question:
<input v-model="question" />
</p>
<p>{{ answer }}</p>
</template>
監視ソースの種類
watch
の第一引数は、リアクティブな「ソース」のさまざまな型に対応しています: その引数には ref(算出 ref も含む)やリアクティブなオブジェクト、getter 関数、あるいは複数のソースの配列といったものです:
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}`)
})
以下のようなリアクティブのオブジェクトのプロパティを監視できないことに注意してください:
const obj = reactive({ count: 0 })
// これは、watch() に数値を渡しているので動作しません。
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
代わりに、getter を使います:
// 代わりに、getter を使います:
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)
Deep Watchers
リアクティブなオブジェクト上で、watch()
関数を直接呼び出すとき、暗黙的に deep watcher が生成されます。 - そのため、コールバックはすべてのネストした変更で実行されます:
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// ネストしたプロパティの変更で実行されます
// 注意: `newValue` は、`oldValue` と同じだとみなされます。
// なぜなら、両者はともに同じオブジェクトを示しているからです。
})
obj.count++
これは、リアクティブなオブジェクトを返す getter により、差別化されます。 - 後者の場合、異なるオブジェクトを返したときにのみコールバックは実行されます。
watch(
() => state.someObject,
() => {
// state.someObject が置き換わった時のみ実行されます。
}
)
しかしながら、deep
オプションを明示的に使用すれば、続いての例で強制的に deep watcher にすることができます。
watch(
() => state.someObject,
(newValue, oldValue) => {
//注意: `newValue` は、`oldValue` と同じだとみなされます。
//state.someObject が置き替わらない*限りは*。
},
{ deep: true }
)
使用上の注意
deep watch は、監視対象のオブジェクトのネストされた全てのプロパティをトラバースする必要があるため、大きなデータ構造で使用するときにはコストが高くなります。使用するときは、どうしても必要なときにだけ使用し、パフォーマンスへの影響に注意しましょう。
watchEffect()
watch()
は遅延して実行されます: 監視対象の値が変更するまでコールバックは実行されません。しかし、同様のコールバックのロジックを先に実行したい場合もあります。- たとえば、初期値のデータを読み込み、関連する状態が変更されるたび、再びデータを読み込みたいときです。気づいたら次のようにしているかもしれません:
const url = ref('https://...')
const data = ref(null)
async function fetchData() {
const response = await fetch(url.value)
data.value = await response.json()
}
// 読み込みがすぐに実行されます。
fetchData()
// そして、url の変更を監視します。
watch(url, fetchData)
これは、watchEffect()
によって簡略化できます。watchEffect()
によって、effect のリアクティブな依存先を自動的に追跡しつつ、副作用をすぐに実行してくれるようになります。上記の例を次のように書き直すことができます:
watchEffect(async () => {
const response = await fetch(url.value)
data.value = await response.json()
})
これで、コールバックはすぐに実行されます。実行している間、依存先(算出プロパティに似たもの)としての url.value
を自動的に追跡してくれます。url.value
が変更されるたびに、コールバックは再実行されます。
この例では、watchEffect
と実際にデータの読み込みがリアクティブに行われている様子をチェックすることができます。
TIP
watchEffect
は同期的な処理中のみ依存先を追跡します。非同期のコールバックで使用する場合、最初の await
の前にアクセスされたプロパティのみが追跡されます。
watch
対 watchEffect
watch
と watchEffect
は、どちらとも副作用をリアクティブに処理することができます。両者の主な違いはリアクティブの依存先の監視方法にあります:
watch
は明示的に示されたソースしか監視しません。コールバック内でアクセスされたものは追跡しません。加えて、コールバックはソースが実際に変更したときにのみ実行されます。watch
は依存性の追跡を副作用から分離します。それにより、コールバックをいつ実行するかをより正確にコントロールすることができます。それに対して、
watchEffect
は依存先の追跡と副作用を 1 つのフェーズにまとめたものになります。同期処理を実行している間にアクセスしたすべてのリアクティブのプロパティを自動的に追跡します。これにより、より便利で一般的にコードが短くなりますが、そのリアクティブの依存先はあまり明示的にならなくなってしまいます。
コールバックが実行されるタイミング
リアクティブな状態が変更されるとき、Vue コンポーネントの更新と生成された watcher コールバックを実行します。
デフォルトでは、ユーザーが生成した watcher のコールバックは Vue コンポーネントが更新される前に呼ばれます。これはつまり、コールバック内で DOM へアクセスしようとすると、DOM は Vue が更新を適用される前の状態です。
もし Vue の更新後に watcher コールバック内で DOM へアクセスしたいとき、flush: 'post'
オプションで指定する必要があります:
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
フラッシュ後の watchEffect()
には、便利なエイリアスである watchPostEffect()
があります:
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* Vue 更新後に実行されます*/
})
Watcher の停止
setup()
, あるいは <script setup>
内に同期的に宣言された watcher により、オーナーコンポーネントのインスタンスにバインドされます。それにより、オーナーコンポーネントがアンマウントされたときに自動的に停止されます。多くの場合、watcher を自分で停止させることを心配する必要はありません。
ここで重要なのは、watcher が同期的に 生成されなければならないということです。もし watcher が非同期なコールバックで生成されたら、オーナーコンポーネントにはバインドされず、メモリーリークを避けるために手動で停止しなければいけないからです。それは次の通りです:
<script setup>
import { watchEffect } from 'vue'
// これは自動的に停止します
watchEffect(() => {})
// ...これは自動では停止しません
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
手動で watcher を停止するには、返されたハンドル関数を使用します。これは、watch
や watchEffect
のどちらにも作用します:
const unwatch = watchEffect(() => {})
// ...あとで、もはや必要なくなったとき
unwatch()
非同期的に watcher を作成する必要にある場合はほとんどになく、可能な限り同期的に作成する方が望ましいことに注意してください。もし非同期のデータを待つ必要があるなら、代わりに watch のロジックを条件付きにすることができます:
// 非同期的にロードされるデータ
const data = ref(null)
watchEffect(() => {
if (data.value) {
// データがロードされたときに何かを実行します
}
})