# Вычисляемые свойства и методы-наблюдатели

В этом разделе в примерах кода используется синтаксис однофайловых компонентов

# Вычисляемые свойства

Иногда требуется состояние, зависящее от другого состояния — во Vue это реализуется с помощью вычисляемых свойств компонента. Но можно создавать вычисляемые свойства и напрямую, с помощью функции computed. Он принимает функцию геттера и возвращает реактивный иммутабельный ref объект для значения, возвращаемого из геттера.


 





const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // ошибка
1
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
1
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++
1
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)
1
2
3
4
5
6
7
8
9

# Остановка отслеживания

Если watchEffect вызывается во время работы функции компонента setup() или во время хуков жизненного цикла, то он привязывается к жизненному циклу компонента и будет автоматически останавливаться при размонтировании компонента.

Для явной остановки отслеживания можно вызвать метод, который он возвращает:

const stop = watchEffect(() => {
  /* ... */
})

// позднее
stop()
1
2
3
4
5
6

# Аннулирование побочных эффектов

Иногда в функции наблюдателя могут быть асинхронные побочные эффекты, которые требуют дополнительных действий при их аннулировании (т.е. в случаях, когда состояние изменилось до того как эффекты завершились). Для таких случаев функция эффекта принимает функцию onInvalidate, которая будет использоваться для аннулирования выполненного и вызываться:

  • когда эффект будет вскоре запущен повторно
  • когда наблюдатель остановлен (т.е. когда компонент размонтирован, если watchEffect используется внутри setup() или хука жизненного цикла)
watchEffect(onInvalidate => {
  const token = performAsyncOperation(id.value)

  onInvalidate(() => {
    // id был изменён или наблюдатель остановлен.
    // аннулирование выполняемой асинхронной операции
    token.cancel()
  })
})
1
2
3
4
5
6
7
8
9

Коллбэк для аннулирования регистрируется передачей функции внутрь, а не возвращением её из коллбэка, потому что для обработки асинхронных ошибок важно возвращаемое значение. Очень часто функция эффекта будет асинхронной при операциях загрузки данных:

const data = ref(null)

watchEffect(async onInvalidate => {
  onInvalidate(() => {
    /* ... */
  }) // регистрируем функцию перед разрешением Promise
  data.value = await fetchData(props.id)
})
1
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>
1
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'
  }
)
1
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
    }
  }
)
1
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) => {
  /* ... */
})
1
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", ""]
1
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 }
}
1
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", ""]
}
1
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]
1
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"
1
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" ""
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Общее поведение с watchEffect

Общее поведение watch и watchEffect — в возможностях остановки отслеживания, аннулировании побочных эффектов (с передачей коллбэка onInvalidate третьим аргументом), синхронизации времени очистки эффектов и инструментов отладки.