計算プロパティを使用すると、派生値を宣言的に計算できます。ただし、場合によっては、状態が変化したときに、DOM を変更したり、非同期操作の結果に基づいて別の場所の状態を変更したりするなど、いくつかの「副作用」を実行する必要があります。
複合 API では、watch 関数を使用して、リアクティブ状態が変化するたびにコールバック関数をトリガーできます。
<script setup>
import {
ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
// 可以直接侦听一个 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 (計算されたプロパティを含む)、リアクティブ オブジェクト、ゲッター関数、または複数のデータ ソースの配列にすることができます。
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() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${
count}`)
})
ここでは、プロパティを返すゲッター関数を使用する必要があります。
// 提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${
count}`)
}
)
ディープリスナー
リアクティブ オブジェクトをwatch() に直接渡すと、暗黙的にディープ リスナーが作成されます。このコールバック関数は、ネストされたすべての変更に対してトリガーされます。
const obj = reactive({
count: 0 })
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})
obj.count++
対照的に、リアクティブ オブジェクトを返すゲッター関数は、別のオブジェクトを返す場合にのみコールバックをトリガーします。
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
)
上記の例に deep オプションを明示的に追加して、強制的にディープ リスナーにすることもできます。
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{
deep: true }
)
慎重に使用してください
ディープ リスニングでは、リスニング対象のオブジェクト内のすべてのネストされたプロパティを走査する必要があり、大規模なデータ構造に使用すると非常にコストがかかります。したがって、必要な場合にのみ使用し、パフォーマンスに注意してください。
即時コールバックのリスナー
デフォルトでは Watch は遅延です。コールバックはデータ ソースが変更された場合にのみ実行されます。ただし、シナリオによっては、リスナーの作成時にコールバックをすぐに実行したい場合があります。たとえば、いくつかの初期データをリクエストし、関連する状態が変化したときにデータを再リクエストしたいとします。
immediate: true
オプションを渡すことで、リスナーのコールバックを強制的に即座に実行できます。
watch(source, (newValue, oldValue) => {
// 立即执行,且当 `source` 改变时再次执行
}, {
immediate: true })
watchEffect()
リスナーのコールバックがソースとまったく同じリアクティブ状態を使用するのが一般的です。たとえば、次のコードは、todoId への参照が変更されるたびに、リスナーを使用してリモート リソースをロードします。
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 })
特に、リスナーが 2 回todoId
(1 回はソースとして、もう 1 回はコールバック内で) 使用されていることに注目してください。
watchEffect 関数を使用すると、上記のコードを簡素化できます。watchEffect() を使用すると、コールバックのリアクティブな依存関係を自動的に追跡できます。上記のリスナーは次のように書き換えることができます。
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 をソース値として明示的に渡す必要がなくなりました。
このような依存関係が 1 つだけある例の場合、watchEffect() の利点は比較的小さいです。ただし、複数の依存関係を持つリスナーの場合、watchEffect() を使用すると、依存関係リストを手動で保守する負担が軽減されます。さらに、ネストされたデータ構造内の複数のプロパティをリッスンする必要がある場合、watchEffect() はすべてのプロパティを再帰的に追跡するのではなく、コールバックで使用されるプロパティのみを追跡するため、ディープ リスナーよりも効率的である可能性があります。
ヒント
watchEffect は同期実行中の依存関係のみを追跡します。非同期コールバックを使用する場合、最初の await が適切に機能する前にアクセスされたプロパティのみが追跡されます。
時計と時計の効果
watch と watchEffect は両方とも、副作用を伴うコールバックをリアクティブに実行できます。それらの主な違いは、リアクティブな依存関係を追跡する方法です。
watch は、明示的にリッスンされるデータ ソースのみを追跡します。コールバックでアクセスされたものは追跡されません。さらに、コールバックは、データ ソースが実際に変更された場合にのみ起動されます。watch は副作用が発生したときに依存関係の追跡を回避するため、コールバック関数がいつトリガーされるかをより正確に制御できます。
watchEffect は、副作用の発生中に依存関係を追跡します。同期中に、アクセス可能なすべてのリアクティブ プロパティが自動的に追跡されます。これはより便利で、コードがすっきりする傾向がありますが、リアクティブな依存関係が明確ではない場合があります。
コールバックのトリガー時間
リアクティブ状態を変更すると、Vue コンポーネントの更新とリスナー コールバックの両方がトリガーされる場合があります。
デフォルトでは、ユーザー作成のリスナー コールバックは、Vue コンポーネントが更新される前に呼び出されます。これは、リスナー コールバックでアクセスする DOM が、Vue によって更新される前の状態になることを意味します。
リスナー コールバックで Vue によって更新された後に DOM にアクセスできるようにしたい場合は、flush: 'post' オプションを指定する必要があります。
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
リフレッシュ後の watchEffect() には、より便利なエイリアス watchPostEffect() があります。
import {
watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
})
リスナーを停止する
<script setup>
setup() の同期ステートメントを使用して作成されたリスナー、またはホスト コンポーネント インスタンスに自動的にバインドされ、ホスト コンポーネントがアンインストールされると自動的に停止します。したがって、ほとんどの場合、リスナーの停止について心配する必要はありません。
重要な点は、リスナーは同期ステートメントを使用して作成する必要があるということです。非同期コールバックを使用してリスナーを作成した場合、リスナーは現在のコンポーネントにバインドされないため、メモリ リークを防ぐために手動で停止する必要があります。次の例のように:
<script setup>
import {
watchEffect } from 'vue'
// 它会自动停止
watchEffect(() => {
})
// ...这个则不会!
setTimeout(() => {
watchEffect(() => {
})
}, 100)
</script>
リスナーを手動で停止するには、watch または watchEffect によって返される関数を呼び出します。
const unwatch = watchEffect(() => {
})
// ...当该侦听器不再需要时
unwatch()
リスナーを非同期的に作成する必要がある状況はほとんどないことに注意してください。可能な限り同期的に作成することを選択してください。非同期データを待つ必要がある場合は、条件付きリスニング ロジックを使用できます。
// 需要异步请求得到的数据
const data = ref(null)
watchEffect(() => {
if (data.value) {
// 数据加载后执行某些操作...
}
})