Наблюдатели
Простой пример
Вычисляемые свойства позволяют нам декларативно вычислять производные значения. Однако бывают случаи, когда нам необходимо выполнить "побочные эффекты" в ответ на изменение состояния. Например, мутировать 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>
Ask a yes/no question:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
</template>
Типы источников watch
Первым аргументом watch
могут быть различные типы реактивных "источников": это может быть ref (включая вычисляемые refs), реактивный объект, геттер-функция или массив из нескольких источников:
js
const x = ref(0)
const y = ref(0)
// одиночный ref
watch(x, (newX) => {
console.log(`x is ${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
}
)
Однако вы можете принудительно преобразовать второй случай в глубокий watcher, явно используя опцию deep
:
js
watch(
() => state.someObject,
(newValue, oldValue) => {
// Примечание: `newValue` здесь будет равно `oldValue`
// *если* state.someObject не был заменен
},
{ deep: true }
)
В Vue 3.5+ опция deep
также может быть числом, указывающим максимальную глубину обхода — то есть на сколько уровней вложенности Vue должен проходить по свойствам объекта.
Используйте с осторожностью
Глубокий наблюдатель требует обхода всех вложенных свойств в просматриваемом объекте и может быть дорогостоящим при использовании на больших структурах данных. Используйте его только в случае необходимости и помните о последствиях для производительности.
Eager Watchers
watch
по умолчанию ленив: обратный вызов не будет вызван, пока не изменится отслеживаемый источник. Но в некоторых случаях мы можем захотеть, чтобы логика обратного вызова выполнялась немедленно - например, мы можем захотеть получить некоторые исходные данные, а затем повторно извлекать данные всякий раз, когда изменяется соответствующее состояние.
Мы можем принудительно выполнить обратный вызов наблюдателя немедленно, передав параметр immediate: true
:
js
watch(
source,
(newValue, oldValue) => {
// выполнится немедленно, и затем при изменении `источника`
},
{ immediate: true }
)
Once Watchers
- Поддерживается только в версиях 3.4+
Обратный вызов наблюдателя будет выполняться всякий раз, когда изменяется отслеживаемый источник. Если вы хотите, чтобы обратный вызов запускался только один раз при изменении источника, используйте параметр once: true
.
js
watch(
source,
(newValue, oldValue) => {
// когда `источник` изменяется, срабатывает только один раз
},
{ 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 }
)
В частности, обратите внимание, что watcher использует 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
и реактивной загрузкой данных в action.
Для примеров с одной зависимостью преимущество watchEffect()
относительно невелико. Но для наблюдателей, у которых есть несколько зависимостей, использование watchEffect()
избавляет от необходимости вести список зависимостей вручную. Кроме того, если вам нужно просмотреть несколько свойств во вложенной структуре данных, watchEffect()
может оказаться более эффективным, чем глубокий наблюдатель, поскольку он будет отслеживать только те свойства, которые используются в обратном вызове, а не рекурсивно отслеживать их все.
Совет
watchEffect
отслеживает зависимости только во время синхронного выполнения. При использовании его с асинхронным обратным вызовом будут отслеживаться только свойства, доступные до первого тика await
.
watch
vs. watchEffect
watch
и watchEffect
оба позволяют нам реактивно выполнять побочные эффекты. Их основное различие заключается в том, как они отслеживают свои реактивные зависимости:
watch
отслеживает только явно указанный источник. Он не будет отслеживать ничего, к чему обращаются внутри обратного вызова. Кроме того, обратный вызов срабатывает только тогда, когда источник действительно изменился.watch
отделяет отслеживание зависимости от побочного эффекта, давая нам более точный контроль над тем, когда должен сработать обратный вызов.watchEffect
, с другой стороны, объединяет отслеживание зависимостей и побочный эффект в одну фазу. Он автоматически отслеживает каждое реактивное свойство, доступ к которому осуществляется во время его синхронного выполнения. Это более удобно и обычно приводит к более лаконичному коду, но делает его реактивные зависимости менее явными.
Side Effect Cleanup
Иногда мы можем получить побочные эффекты, например при асинхронных запросах:
js
watch(id, (newId) => {
fetch(`/api/${newId}`).then(() => {
// callback logic
})
})
Но что, если id
изменится до завершения запроса? Когда запрос завершится, мы получим данные с предыдущем запрошенным значением. В идеале, мы хотим иметь возможность отменить запрос, при изменении id
.
Мы можем использовать onWatcherCleanup()
API для регистрации функции очистки, которая будет вызываться, когда наблюдатель становится недействительным и собирается перезапуститься:
js
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// callback logic
})
onWatcherCleanup(() => {
// abort stale request
controller.abort()
})
})
Обратите внимание, что onWatcherCleanup
поддерживается только в Vue 3.5+ и должен вызываться во время синхронного вызова функции эффекта watchEffect
или обратного вызова watch
. Ты не сможешь его вызвать после await
в асинхронной функции.
Альтернативно, в функцию onCleanup
передается в колбэк watch
в качестве третьего аргумент, и в watchEffect
в качестве первого аргумента:
js
watch(id, (newId, oldId, onCleanup) => {
// ...
onCleanup(() => {
// cleanup logic
})
})
watchEffect((onCleanup) => {
// ...
onCleanup(() => {
// cleanup logic
})
})
Это работает в версии до 3.5. Кроме того, onCleanup
, передаваемый через аргумент функции, привязан к экземпляру наблюдателя, поэтому на него не распространяется синхронное ограничение onWatcherCleanup
.
Время обратного вызова
Когда вы изменяете реактивное состояние, это может вызвать как обновления компонентов Vue, так и обратные вызовы наблюдателя, созданные вами.
Подобно обновлению компонентов, созданные пользователем обратные вызовы наблюдателей обрабатываются пакетно, чтобы избежать дублирования. Например, мы, вероятно, не хотим, чтобы наблюдатель запускался тысячу раз, если мы синхронно помещаем тысячу элементов в отслеживаемый массив.
По умолчанию, созданные пользователем обратные вызовы наблюдателей вызываются до обновления компонентов Vue. Это означает, что если вы попытаетесь получить доступ к DOM внутри обратного вызова наблюдателя, DOM будет находиться в состоянии до того, как Vue применит какие-либо обновления.
Post Watchers
Если вы хотите получить доступ к DOM в обратном вызове наблюдателя после того, как Vue обновит его, вам нужно указать опцию flush: 'post'
:
js
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
Post-flush watchEffect()
также имеет удобный псевдоним, watchPostEffect()
:
js
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* выполняется после обновлений Vue */
})
Sync watchers
Также можно создать наблюдатель, который будет срабатывать синхронно, перед любыми обновлениями, управляемыми Vue:
js
watch(source, callback, {
flush: 'sync'
})
watchEffect(callback, {
flush: 'sync'
})
У синхронных watchEffect()
также есть удобный псевдоним, watchSyncEffect()
:
js
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
/* выполняется синхронно при изменении реактивных данных */
})
Используйте с осторожностью
Синхронные наблюдатели не имеют пакетной обработки и срабатывают каждый раз, когда обнаруживается реактивная мутация. Их можно использовать для наблюдения за простыми булевыми значениями, но избегайте их использования для источников данных, которые могут синхронно изменены много раз, например, массивов.
Остановка наблюдателя
Наблюдатели, объявленные синхронно внутри setup()
или <script setup>
, привязываются к экземпляру компонента-владельца и автоматически останавливаются, когда компонент-владелец размонтируется. В большинстве случаев вам не нужно беспокоиться о том, чтобы остановить наблюдателя самостоятельно.
Ключевым моментом здесь является то, что наблюдатель должен быть создан синхронно. Если наблюдатель будет создан в асинхронном обратном вызове, он не будет привязан к компоненту-владельцу и должен быть остановлен вручную, чтобы избежать утечки памяти. Вот пример:
vue
<script setup>
import { watchEffect } from 'vue'
// будет автоматически остановлено
watchEffect(() => {})
// ...это - нет!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
Чтобы вручную остановить watcher, используйте функцию возврата. Это работает как для watch
, так и для watchEffect
:
js
const unwatch = watchEffect(() => {})
// ...позже, когда уже не нужно
unwatch()
Обратите внимание, что случаев, когда вам нужно создавать наблюдатели асинхронно, должно быть очень мало, и по возможности лучше предпочесть синхронное создание. Если вам нужно дождаться асинхронных данных, вы можете сделать логику работы наблюдателя условной:
js
// данные, загружаемые асинхронно
const data = ref(null)
watchEffect(() => {
if (data.value) {
// делать что-то при загрузке данных
}
})