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

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

# Вычисляемые значения

Иногда требуется состояние, зависящее от другого состояния — во Vue это реализуется с помощью свойства computed компонента. Для создания вычисляемого значения напрямую можно использовать метод 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

Кроме того, можно передать объект с функциями get и set для создания изменяемого ref объекта.

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

# 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

Асинхронная функция неявно возвращает 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', которое принудительно заставит эффект всегда срабатывать синхронно. Однако это неэффективно и должно использоваться крайне редко.

# Отладка наблюдателей

Опции 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

# Отслеживание реактивных объектов

Использование наблюдателя для сравнения значений массива или объекта, которые являются реактивными, требуют, чтобы у него была копия, состоящая только из значений.

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

Чтобы отслеживать изменения свойств в глубоко вложенном объекте или массиве нужно установить опцию 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"; // Logs: "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
24
25
26
27
28
29
30
31

Однако, при отслеживании реактивного объекта или массива будет всегда возвращаться одна ссылка на текущее значение этого объекта как для текущего, так и для предыдущего состояния. Для полноценного отслеживания глубоко вложенных объектов или массивов, может потребоваться создание глубокой копии значений. Это может сделать с помощью утилиты, такой как 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
18
19
20

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

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

Deployed on Netlify.
Последнее обновление: 2021-02-25, 21:43:15 UTC