Наблюдатели
Пример использования
Вычисляемые свойства позволяют нам декларативно вычислять производные значения. Однако бывают случаи, когда нам необходимо выполнить «побочные эффекты» в ответ на изменения состояния — например, изменить DOM или изменить другую часть состояния на основе результата асинхронной операции.
С помощью Composition API мы можем использовать функцию watch
для запуска обратного вызова при каждом изменении части реактивного состояния:
vue
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Вопросы обычно содержат вопросительный знак. ;-)')
const loading = ref(false)
// watch работает напрямую с ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
loading.value = true
answer.value = 'Думаю...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Ошибка! Не удалось связаться с API. ' + error
} finally {
loading.value = false
}
}
})
</script>
<template>
<p>
Задайте вопрос «да/нет»:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
</template>
Типы источников Watch
Первым аргументом watch
могут быть различные типы реактивных «источников»: Это может быть ссылка (включая вычисляемые ссылки), реактивный объект, геттер-функция или массив из нескольких источников:
js
const x = ref(0)
const y = ref(0)
// одиночная ссылка
watch(x, (newX) => {
console.log(`x — ${newX}`)
})
// геттер
watch(
() => x.value + y.value,
(sum) => {
console.log(`сумма x + y: ${sum}`)
}
)
// массив из нескольких источников
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x — ${newX}, y — ${newY}`)
})
Обратите внимание, что вы не можете наблюдать за свойством реактивного объекта таким образом:
js
const obj = reactive({ count: 0 })
// Это не сработает, потому что мы передаем в watch() число
watch(obj.count, (count) => {
console.log(`Счётчик: ${count}`)
})
Вместо этого используйте геттер:
js
// используем геттер:
watch(
() => obj.count,
(count) => {
console.log(`Счётчик: ${count}`)
}
)
Глубокие наблюдатели
Когда вы вызываете watch()
непосредственно на реактивном объекте, он неявно создаст глубокий наблюдатель — обратный вызов будет срабатывать на все вложенные мутации:
js
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// срабатывает при мутациях вложенных свойств
// Примечание: Здесь `newValue` будет равно `oldValue`,
// потому что они оба указывают на один и тот же объект!
})
obj.count++
Это следует отличать от геттера, возвращающего реактивный объект — в последнем случае обратный вызов сработает только в том случае, если геттер вернет другой объект:
js
watch(
() => state.someObject,
() => {
// срабатывает только при замене state.someObject
}
)
Однако вы можете заставить второй вариант превратиться в глубокий наблюдатель, явно используя опцию deep
:
js
watch(
() => state.someObject,
(newValue, oldValue) => {
// Примечание: Здесь `newValue` будет равно `oldValue`,
// *если* state.someObject не был заменен
},
{ deep: true }
)
В Vue 3.5+ опция deep
также может быть числом, указывающим на максимальную глубину обхода — т. е. на сколько уровней Vue должен обойти вложенные свойства объекта.
Используйте с осторожностью
Глубокое наблюдение требует обхода всех вложенных свойств в наблюдаемом объекте и может быть дорогостоящим при использовании больших структур данных. Используйте его только в случае необходимости и остерегайтесь последствий для производительности.
Нетерпеливые наблюдатели
По умолчанию 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
:
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
дважды: один раз в качестве источника, а затем ещё раз внутри обратного вызова.
Это можно упростить с помощью 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()
и реактивной выборки данных в действии.
Для таких примеров, как этот, с одной зависимостью, польза от watchEffect()
относительно невелика. Но для наблюдателей, у которых есть несколько зависимостей, использование watchEffect()
снимает бремя необходимости вести список зависимостей вручную. Кроме того, если вам нужно следить за несколькими свойствами во вложенной структуре данных, watchEffect()
может оказаться более эффективным, чем глубокий наблюдатель, поскольку он будет отслеживать только те свойства, которые используются в обратном вызове, а не рекурсивно отслеживать их все.
Примечание
watchEffect
отслеживает зависимости только во время своего синхронного выполнения. При использовании его с асинхронным обратным вызовом будут отслеживаться только свойства, доступные до первого тика await
.
watch
в сравнении с watchEffect
watch
и watchEffect
позволяют нам реактивно выполнять побочные эффекты. Их основное отличие заключается в способе отслеживания реактивных зависимостей:
watch
отслеживает только явно наблюдаемый источник. Он не будет отслеживать ничего, к чему обращаются внутри обратного вызова. Кроме того, обратный вызов срабатывает только тогда, когда источник действительно изменился.watch
отделяет отслеживание зависимостей от побочного эффекта, предоставляя нам более точный контроль над тем, когда должен сработать обратный вызов.С другой стороны,
watchEffect
объединяет отслеживание зависимостей и побочных эффектов в одну фазу. Он автоматически отслеживает каждое реактивное свойство, к которому обращаются во время его синхронного выполнения. Это удобнее и обычно приводит к более короткому коду, но делает реактивные зависимости менее явными.
Очистка от побочных эффектов
Иногда мы можем выполнять побочные действия, например асинхронные запросы в наблюдателе:
js
watch(id, (newId) => {
fetch(`/api/${newId}`).then(() => {
// логика обратного вызова
})
})
Но что, если id
изменится до завершения запроса? Когда предыдущий запрос завершится, он всё равно вызовет обратный вызов со значением ID, которое уже устарело. В идеале, мы хотим иметь возможность отменить устаревший запрос, когда id
изменится на новое значение.
Можно использовать API onWatcherCleanup()
для регистрации функции очистки, которая будет вызвана, когда наблюдатель станет недействительным и будет запущен снова:
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
в async-функции.
Кроме того, функция onCleanup
также передается в обратные вызовы наблюдателя в качестве 3-го аргумента, а в функцию эффекта watchEffect
— в качестве первого аргумента:
js
watch(id, (newId, oldId, onCleanup) => {
// ...
onCleanup(() => {
// логика обратного вызова
})
})
watchEffect((onCleanup) => {
// ...
onCleanup(() => {
// логика обратного вызова
})
})
Это работает в версиях до 3.5. Кроме того, onCleanup
, переданный через аргумент функции, привязан к экземпляру наблюдателя, поэтому на него не распространяется ограничение синхронности onWatcherCleanup
.
Время сброса обратного вызова
Когда вы изменяете реактивное состояние, это может вызвать как обновления компонентов Vue, так и обратные вызовы наблюдателей, созданные вами.
По умолчанию созданные пользователем обратные вызовы наблюдателя вызываются перед обновлениями компонентов Vue. Это означает, что если вы попытаетесь получить доступ к DOM внутри обратного вызова наблюдателя, DOM будет находиться в состоянии до того, как Vue применит какие-либо обновления.
Пост-наблюдатели
Если вы хотите получить доступ к DOM в обратном вызове наблюдателя после обновления Vue, вам нужно указать опцию flush: 'post'
:
js
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
watchEffect()
с опцией flush: 'post'
также имеет удобный псевдоним — watchPostEffect()
:
js
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* выполняется после обновлений Vue */
})
Наблюдатели синхронизации
Также возможно создать наблюдатель, который срабатывает синхронно перед любыми обновлениями, управляемыми Vue:
js
watch(source, callback, {
flush: 'sync'
})
watchEffect(callback, {
flush: 'sync'
})
watchEffect()
с опцией flush: 'sync'
также имеет удобный псевдоним — 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) {
// делать что-то при загрузке данных
}
})