Эффекты

computed()

Принимает на вход функцию, вычисляющую что-либо на основе реактывных данных и возвращает вычисляемую ref ссылку на результат. В случае изменения реактивных данных, учавствовавших в вычислениях, функция будет вызвана повторно и обновит вычесленый результа.

import { ref, computed } from "vue";

const a = ref(1);
const b = ref(2);

const sum = computed(() => a.value + b.value);
console.log(sum.value); // 3

a.value = 3;
console.log(sum.value); // 4

записываемая computed ссылка

На вход computed() можно передать две функции get и set

const count = ref(10);
const doubleCount = computed({
  get: () => count.value * 2,
  set: (val) => {
    count.value = val / 2;
  },
});

console.log(doubleCount.value); // 20
count.value = 15;
console.log(doubleCount.value); // 30
doubleCount.value = 40;
console.log(count.value); // 20

watchEffect()

Функция watchEffect() немедленно запускает переданную в нее функцию, запоминает все реактивные объекты исппользовавшиеся по вызове, и при измененнии любого из этих объектов опять запускает исходную функцию.

const count = ref(10);
const doubleCount = ref();

watchEffect(() => (doubleCount.value = count.value * 2));

console.log(doubleCount.value); //  20

count.value = 15;
console.log(doubleCount.value); //  30

watch()

Функция watch во многом похожа на watchEffect но есть несколько отличий:

  • watch ленив по умолчанию. Первый вызов обработчика происходит не в момент его установки, а лишь при ппервом изменении данных.
  • watch позволяет контролировать на какие изменения стоит реагировать
  • watch получает старое и новое значение наблюдаемых переменных.

Наблюдение за ref-ссылкой

const count = ref(0);
const count2 = ref(0);

watch(count, (count, prevCount) => {
  console.log(`Counter changed from ${prevCount} to ${count}`);
  console.log(`Counter 2 is ${count2.value}`);
});

count.value = 1; // Counter changed from 0 to 1
// Counter 2 is 0`
count2.value = 1; // не вызовет сработку отслеживания, т.к. count2 не включен в список для наблюдения

Наблюдение за геттер-функцией

Для наблюдение за реактивными данными, необходимо обернуть эти данные в геттер-функцию и наблюдать за ней.

const state = reactive({ count: 0 });
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
);

const numbers = reactive([1, 2, 3, 4]);
watch(
  () => [...numbers],
  (numbers, prevNumbers) => {
    console.log(numbers, prevNumbers);
  }
);

Глубокое наблюдение за реактывным объектом

Можно установить параметр 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"

Однако в таком случае невозможно получить старое значение переменных. Для полноценного отслеживания глубоко вложенных объектов или массивов, может потребоваться создавать глубокую копию значений.

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" ""

Отслеживание нескольких переменных

const fooRef = ref(0);
const barRef = ref(0);
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
});

При одновременном изменении обоих наблюдаемых источников в одной и той же функции, наблюдатель будет вызван только один раз. Если необходимо два раза вызвать обработчик, то можно использовать flush: 'sync', но это не рекомендуется. Можно вызвать nextTick, после первого изменения чтобы дожидаться срабатывания наблюдателя.

const firstName = ref("");
const lastName = ref("");

watch([firstName, lastName], (newValues, prevValues) => {
  console.log(newValues, prevValues);
});

const changeValues = () => {
  firstName.value = "John";
  lastName.value = "Smith";
};

changeValues(); // выведет в консоль: ["John", "Smith"] ["", ""]

const changeValues2 = async () => {
  firstName.value = "John"; // выведет в консоль: ["John", ""] ["", ""]
  await nextTick();
  lastName.value = "Smith"; // выведет в консоль: ["John", "Smith"] ["John", ""]
};
changeValues2(); // выведет в консоль: ["John", ""] ["", ""], затем : ["John", "Smith"] ["John", ""]

Остановка отслеживания (watchEffect(), watch())

При использовании watchEffect и watch внутри script setup или во время хуков жизненного цикла, они будет автоматически останавливаться при размонтировании компонента.

Так же можно вручную остановить отслеживане:

const stopWatchEffect = watchEffect(() => {
  /* ... */
});

const stopWatch = watch(test, (new, old) => {
  /* ... */
});


function onBtnClick() {
  stopWatchEffect();
  stopWatch();
}

Аннулирование побочных эффектов (watchEffect(), watch())

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

const data = ref(null);
const canceled = ref(false);

watchEffect(async (onInvalidate) => {
  onInvalidate(() => {
    canceled.value = true;
  }); // регистрируем функцию перед разрешением Promise
  data.value = await fetchData(props.id);
  canceled.value = false;
});

Отладка эффектов (computed(), watchEffect(), watch() )

Для отладки эффектов в режиме разработки можно передать дополнительный аргумент с опциями 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++;

Свойство flush (watchEffect(), watch())

Свойство flush определяет, когда запускается эффект: до, после или во время повторного рендеринга страницы.

let stop = watchEffect(callback, {
  flush: "pre",
});
  • pre - функция обработчик вызываются ДО обновления дерева DOM (по умолчанию).
  • post - функция обработчик вызываются ПОСЛЕ обновления дерева DOM. Данное поведение полезно при работе со ссылками на элемента шаблона
  • sync - принудительно заставит эффект всегда срабатывать синхронно. Однако такое поведение неэффективно и должно использоваться только в крайних случаях.

watchPostEffect и watchSyncEffect

Vue3.2+

Псевдонимы для watchEffect c предустановленными значениями flush