# Вычисляемые свойства и методы-наблюдатели
В этом разделе в примерах кода используется синтаксис однофайловых компонентов
# Вычисляемые свойства
Иногда требуется состояние, зависящее от другого состояния — во Vue это реализуется с помощью вычисляемых свойств компонента. Но можно создавать вычисляемые свойства и напрямую, с помощью функции computed
. Он принимает функцию геттера и возвращает реактивный иммутабельный ref объект для значения, возвращаемого из геттера.
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // ошибка
2
3
4
5
6
Для создания изменяемого ref-объекта можно передать объект с функциями get
и set
.
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
2
3
4
5
6
7
8
9
10
# Отладка вычисляемых свойств 3.2+
Для отладки computed
принимает второй аргумент с опциями onTrack
и onTrigger
:
onTrack
вызывается, когда реактивное свойство или ссылка отслеживается как зависимость.onTrigger
вызывается, когда коллбэк наблюдателя будет вызван изменением зависимости.
Оба коллбэка получают debugger-событие с информацией о зависимости. Рекомендуется указывать в этих коллбэках оператор debugger
для интерактивной проверки зависимости:
const plusOne = computed(() => count.value + 1, {
onTrack(e) {
// срабатывает, когда count.value отслеживается как зависимость
debugger
},
onTrigger(e) {
// срабатывает при изменении значения count.value
debugger
}
})
// доступ к plusOne вызовет срабатывание onTrack
console.log(plusOne.value)
// изменение count.value вызовет срабатывание onTrigger
count.value++
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Обратите внимание, что onTrack
и onTrigger
работают только в режиме разработки.
# watchEffect
Чтобы применить и автоматически применить повторно побочный эффект, который основан на реактивном состоянии, можно использовать функцию watchEffect
. Он запускает функцию немедленно при отслеживании своих зависимостей и будет повторно запускать её при изменении одной из зависимостей.
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> выведет в консоль 0
setTimeout(() => {
count.value++
// -> выведет в консоль 1
}, 100)
2
3
4
5
6
7
8
9
# Остановка отслеживания
Если watchEffect
вызывается во время работы функции компонента setup() или во время хуков жизненного цикла, то он привязывается к жизненному циклу компонента и будет автоматически останавливаться при размонтировании компонента.
Для явной остановки отслеживания можно вызвать метод, который он возвращает:
const stop = watchEffect(() => {
/* ... */
})
// позднее
stop()
2
3
4
5
6
# Аннулирование побочных эффектов
Иногда в функции наблюдателя могут быть асинхронные побочные эффекты, которые требуют дополнительных действий при их аннулировании (т.е. в случаях, когда состояние изменилось до того как эффекты завершились). Для таких случаев функция эффекта принимает функцию onInvalidate
, которая будет использоваться для аннулирования выполненного и вызываться:
- когда эффект будет вскоре запущен повторно
- когда наблюдатель остановлен (т.е. когда компонент размонтирован, если
watchEffect
используется внутриsetup()
или хука жизненного цикла)
watchEffect(onInvalidate => {
const token = performAsyncOperation(id.value)
onInvalidate(() => {
// id был изменён или наблюдатель остановлен.
// аннулирование выполняемой асинхронной операции
token.cancel()
})
})
2
3
4
5
6
7
8
9
Коллбэк для аннулирования регистрируется передачей функции внутрь, а не возвращением её из коллбэка, потому что для обработки асинхронных ошибок важно возвращаемое значение. Очень часто функция эффекта будет асинхронной при операциях загрузки данных:
const data = ref(null)
watchEffect(async onInvalidate => {
onInvalidate(() => {
/* ... */
}) // регистрируем функцию перед разрешением Promise
data.value = await fetchData(props.id)
})
2
3
4
5
6
7
8
Асинхронная функция неявно возвращает Promise, но зарегистрировать функцию для очистки нужно перед разрешением Promise. Кроме того, Vue полагается на возвращаемый Promise для автоматической обработки потенциальных ошибок в цепочке Promise.
# Синхронизация времени очистки эффектов
Система реактивности Vue буферизирует аннулированные эффекты и выполняет их очистку асинхронно. Это сделано для избежания повторяющихся вызовов, когда в одном «тике» происходит много изменений состояния. Внутренняя функция компонента update
также является эффектом. При добавлении пользовательского эффекта в очередь, по умолчанию он будет вызываться перед всеми эффектами update
компонента:
<template>
<div>{{ count }}</div>
</template>
<script>
export default {
setup() {
const count = ref(0)
watchEffect(() => {
console.log(count.value)
})
return {
count
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
В этом примере:
- Значение счётчика будет выведено в консоль синхронно при первом запуске.
- При изменениях
count
, коллбэк будет вызываться перед обновлением компонента.
В случаях, когда эффект наблюдателя требуется повторно запускать после обновления компонента (например, при работе со ссылками на элемента шаблона), можно передать дополнительный объект настроек с опцией flush
(значение по умолчанию — 'pre'
):
// Будет вызываться после обновления компонента,
// поэтому можно получить доступ к обновлённому DOM
// Примечание: это также отложит первоначальный запуск эффекта
// до тех пор, пока первая отрисовка компонента не будет завершена.
watchEffect(
() => {
/* ... */
},
{
flush: 'post'
}
)
2
3
4
5
6
7
8
9
10
11
12
Опция flush
также может принимать значение 'sync'
, которое принудительно заставит эффект всегда срабатывать синхронно. Однако такое поведение неэффективно и должно использоваться в крайних случаях.
С версии Vue >= 3.2.0, можно использовать псевдонимы watchPostEffect
и watchSyncEffect
, чтобы сделать код более понятным.
# Отладка наблюдателей
Можно использовать опции onTrack
и onTrigger
для отладки поведения наблюдателя.
onTrack
вызывается, когда реактивное свойство или ссылка начинает отслеживаться как зависимость.onTrigger
вызывается, когда коллбэк наблюдателя вызван изменением зависимости.
Оба коллбэка получают событие отладчика с информацией о зависимости, о которой идёт речь. Рекомендуем указывать debugger
в них для удобного инспектирования зависимости:
watchEffect(
() => {
/* побочный эффект */
},
{
onTrigger(e) {
debugger
}
}
)
2
3
4
5
6
7
8
9
10
Обратите внимание, опции onTrack
и onTrigger
работают только в режиме разработки.
# watch
API watch
является полным эквивалентом свойства watch компонента. watch
требуется конкретный источник данных для наблюдения и выполняет побочные эффекты в отдельной функции коллбэка. Он также ленив по умолчанию — т.е. коллбэк вызывается только тогда, когда наблюдаемый источник изменился.
По сравнению с watchEffect,
watch
позволяет:- Лениво выполнять побочные эффекты;
- Точнее определять какое состояние должно вызвать перезапуск;
- Получать доступ к предыдущему и текущему значению наблюдаемого состояния.
# Отслеживание одного источника данных
В качестве источника данных для наблюдателя можно указать функцию-геттер, которая вернёт значение, или непосредственно реактивную ссылку ref
:
// наблюдение за геттер-функцией
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// наблюдение за ref-ссылкой
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
2
3
4
5
6
7
8
9
10
11
12
13
14
# Отслеживание нескольких источников данных
Можно отслеживать также и несколько источников одновременно, используя синтаксис наблюдателя с массивом:
const firstName = ref('')
const lastName = ref('')
watch([firstName, lastName], (newValues, prevValues) => {
console.log(newValues, prevValues)
})
firstName.value = 'John' // выведет в консоль: ["John", ""] ["", ""]
lastName.value = 'Smith' // выведет в консоль: ["John", "Smith"] ["John", ""]
2
3
4
5
6
7
8
9
Однако, при одновременном изменении обоих наблюдаемых источников в одной и той же функции, наблюдатель будет вызван только один раз:
setup() {
const firstName = ref('')
const lastName = ref('')
watch([firstName, lastName], (newValues, prevValues) => {
console.log(newValues, prevValues)
})
const changeValues = () => {
firstName.value = 'John'
lastName.value = 'Smith'
// выведет в консоль: ["John", "Smith"] ["", ""]
}
return { changeValues }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Обратите внимание, что при нескольких синхронных изменениях наблюдатель срабатывает только один раз.
Можно форсировать срабатывание наблюдателя после каждого изменения, используя опцию flush: 'sync'
, хотя её применение не рекомендуется. В качестве альтернативы можно воспользоваться nextTick, чтобы дожидаться срабатывания наблюдателя перед внесением следующих изменений. Например:
const changeValues = async () => {
firstName.value = 'John' // выведет в консоль: ["John", ""] ["", ""]
await nextTick()
lastName.value = 'Smith' // выведет в консоль: ["John", "Smith"] ["John", ""]
}
2
3
4
5
# Отслеживание реактивных объектов
Для сравнения значений массива или объекта, которые являются реактивными, наблюдателю потребуются, чтобы у него была копия, состоящая только из значений.
const numbers = reactive([1, 2, 3, 4])
watch(
() => [...numbers],
(numbers, prevNumbers) => {
console.log(numbers, prevNumbers)
}
)
numbers.push(5) // Выведет в консоль: [1,2,3,4,5] [1,2,3,4]
2
3
4
5
6
7
8
9
10
При необходимости отслеживать изменения свойств в глубоко вложенном объекте или массиве нужно установить опцию deep
в значение true
:
const state = reactive({
id: 1,
attributes: {
name: '',
}
})
watch(
() => state,
(state, prevState) => {
console.log('без опции deep ', state.attributes.name, prevState.attributes.name)
}
)
watch(
() => state,
(state, prevState) => {
console.log('с опцией deep ', state.attributes.name, prevState.attributes.name)
},
{ deep: true }
)
state.attributes.name = 'Alex' // выведет в консоль: "с опцией deep " "Alex" "Alex"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Однако, при отслеживании реактивного объекта или массива будет всегда возвращаться одна ссылка на текущее значение этого объекта как для текущего, так и для предыдущего состояния. Для полноценного отслеживания глубоко вложенных объектов или массивов, может потребоваться создавать глубокую копию значений. Это можно сделать например с помощью утилиты lodash.cloneDeep (opens new window)
import _ from 'lodash'
const state = reactive({
id: 1,
attributes: {
name: '',
}
})
watch(
() => _.cloneDeep(state),
(state, prevState) => {
console.log(state.attributes.name, prevState.attributes.name)
}
)
state.attributes.name = 'Alex' // Выведет в консоль: "Alex" ""
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Общее поведение с watchEffect
Общее поведение watch
и watchEffect
— в возможностях остановки отслеживания, аннулировании побочных эффектов (с передачей коллбэка onInvalidate
третьим аргументом), синхронизации времени очистки эффектов и инструментов отладки.