Привет! Меня зовут Алексей, я frontend-специалист SimbirSoft. В этой статье разберем новый метод слежения за реактивными свойствами watchEffect.

С появлением Vue 3 c Composition API стало доступно два метода слежения — watch и watchEffect. Если «старый» метод watch всем хорошо знаком и не должен вызывать затруднений у Vue-разработчиков, то новый метод стоит изучить подробнее. Материал будет полезен разработчикам, переходящим с Vue 2 на Vue 3 и всем «вьюшникам», которые еще не разобрались с этим методом.

Composition API предоставляет нам два разных метода слежения за реактивными свойствами — watch и watchEffect. Они похожи, но все же каждый полезен в определенных случаях. Рассмотрим, какие сходства и различия существуют у этих методов:

Общее

Отличия

● следят за изменениями зависимостей
● выполняют побочные эффекты в отдельной функции коллбэка
● предоставляют способ остановки слежения

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

Это были основные отличия, о других мы расскажем ниже.

Слежение за объектами и массивами в watchEffect

WatchEffect может отслеживать только адрес памяти реактивного объекта. Изменение элементов массива или свойств объекта не изменит адрес памяти и, следовательно, не вызовет срабатывания метода watchEffect:

<template>
   <div class="home">
     <button @click="addTarget">add</button>    
     <div>{{ target }}</div>  
   </div>
</template>

<script setup>
import { reactive, watchEffect } from "vue";

const target = reactive([1, 2, 3, 4]);

watchEffect(() => {  
   console.log(target);
});

const addTarget = () => {  
   target.push(5);
};
</script>

При нажатии на кнопку add видим, что данные в массив добавляются, но watchEffect отрабатывает только один раз. WatchEffect всегда срабатывает только один раз после монтажа компонента, в watch нам приходилось указывать для этого immediate — true:

Нам нужно преобразовать реактивный объект обратно в массив с помощью spread-оператора:

<template>
  <div class="home">
    <button @click="addTarget">add</button>
    {{ target }}  
  </div>
</template>

<script setup>
import { reactive, watchEffect } from "vue";

const target = reactive([1, 2, 3, 4]);

watchEffect(() => {  
  const arr = [...target];  
  console.log(arr);
});

const addTarget = () => {
  target.push(5);};
</script>

Теперь наш watchEffect успешно отрабатывает:

Для слежения за объектами будем использовать toRefs():

<template>  
  <div class="home">    
    <button @click="setTitle">changeTitle</button>
    {{ data }}  
  </div>
</template>

<script setup>
import { reactive, watchEffect, toRefs } from "vue";

const data = reactive({  
  title: "Some title",  
  desc: "Some description",
});

watchEffect(() => {  
  const parse = toRefs(data);  
  console.log(parse);
});

const setTitle = () => {  
  data.title = "New title";
};
</script>

Вызов функции stop() остановит действие watchEffect:

<template>  
  <div class="home">    
    <button @click="setTitle">changeTitle</button>    
    <button @click="stop">stop</button>
    <div>      
      {{ data }}    
    </div>  
  </div>
</template>

<script setup>
import { reactive, watchEffect, toRefs } from "vue";

const data = reactive({  
  title: "Some title",  
  desc: "Some description",
});

const stopWatchEffect = watchEffect(() => {  
  const parse = toRefs(data);  
  console.log(parse);
});

const setTitle = () => {  
  data.title = "New title";
};

const stop = () => {  
  stopWatchEffect();
};
</script>

Параметры watchEffect

Метод watchEffect принимает два аргумента. Первый — это коллбэк-функция. Второй — объект конфигурации:

watchEffect(
() => {},
{  
  flush: 'post',  
  onTrack(e) {    
    debugger  
  },
  onTrigger(e) {    
    debugger  
  }
})

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

flush: 'pre' | 'post' | 'sync'

По умолчанию созданные пользователем коллбек-функции наблюдателя вызываются до обновления компонентов Vue. Это означает, что если вы попытаетесь получить доступ к DOM внутри коллбек-функции наблюдателя, DOM будет в состоянии до того, как Vue применит какие-либо обновления.

Дополнительный объект настроек с опцией flush (значение по умолчанию — 'pre'):

let stop = watchEffect(callback, {  
  flush: 'pre'
})

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

watchEffect(callback, {  
  flush: 'sync'
})

Если вы хотите получить доступ к DOM в коллбек-функции наблюдателя после того, как Vue его обновил его, вам нужно указать flush: 'post':

watchEffect(callback, {  
  flush: 'post'
})

Отладка watchEffect

Можно использовать опции onTrack и onTrigger для отладки поведения наблюдателя:

  • onTrack вызывается, когда реактивное свойство или ссылка начинает отслеживаться как зависимость;

  • onTrigger вызывается, когда коллбэк наблюдателя вызван изменением зависимости.

Оба коллбэка получают событие отладчика с информацией о зависимости, о которой идет речь. Обратите внимание, опции onTrack и onTrigger работают только в режиме разработки.

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

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

  • когда эффект будет вскоре запущен повторно;

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

<template>
  <div class="home">
    <button @click="setTitle">changeTitle</button>    
    <button @click="stop">stop</button>    
    {{ data }}  
  </div>
</template>

<script setup>
import { reactive, watchEffect, toRefs } from "vue";

const data = reactive({
  title: "Some title",  
  desc: "Some description",
});

const stopWatchEffect = watchEffect((onInvalidate) => {  
  const parse = toRefs(data);  
  console.log(parse);  
  console.log("basic function");  
  
  onInvalidate(() => {    
    console.log("onInvalidate function");  
  });
});

const setTitle = () => {  
  data.title = "New title";
};

const stop = () => {  
  stopWatchEffect();
};
</script>

Снова запустим наше приложение:

Видим, что сначала отработала основная коллбек-функция, и в консоли вывелось basic function.

Если мы нажмем на кнопку stop, то сработает функция onInvalidate:

Если нажать на кнопку change title, то вначале выполнится функция onInvalidate, а затем основная функция. При изменении свойств onInvalidate будет всегда срабатывать первой:

Практическое применение watchEffect

Мы будем отменять запросы axios, если входные данные изменились, и старые данные больше не актуальны:

<template>  
  <div class="home">    
    <div>      
      <button @click="setType('albums')">load albums</button>      
      <button @click="setType('posts')">load post</button>      
      <button @click="stop">stop</button>    
    </div>    
    <div>      
        {{ information }}
    </div>  
  </div>
</template>

<script setup>
import { ref, watchEffect } from "vue";
import axios from "axios";

const type = ref("albums");
const information = ref(null);
const source = ref(null);

const setType = (value) => {  
  type.value = value;
};

const init = async function () {
  source.value = axios.CancelToken.source();  
  try {    
    const data = await axios.get(      
      `https://jsonplaceholder.typicode.com/${type.value}`,
      {        
        cancelToken: source.value.token,
      }
    );    
    return data;  
  } catch (error) {
    console.log(error);  
  }
};

const stop = watchEffect((onInvalidate) => {  
  onInvalidate(() => {    
    source.value.cancel("Отмена запроса");  
  });  
  
  init(type.value).then((res) => {    
    information.value = res;  
  });
});
</script>

Функция setType изменит значение переменной type в зависимости от параметра, который был принят. WatchEffect следит за изменением переменной type, и при ее изменении делает запрос на получение постов или альбомов. Заметьте, мы нигде изначально не инициализируем запрос, поскольку watchEffect срабатывает один раз сразу при загрузке страницы:

Установим скорость сети в Slow 3G. И будем быстро переключаться по кнопкам load post и load alboms. Мы увидим, что запросы, которые больше не актуальны, отменяются.

Резюме

Мы разобрались с методом наблюдения за реактивными сущностями с помощью watchEffect.

Что следует запомнить о методе watchEffect:

  • он всегда срабатывает один раз после монтажа компонента;

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

  • если вы хотите отслеживать изменения после обновления компонента, используйте опцию flush: 'post';

  • функция onInvalidate всегда срабатывает перед основной функцией;

  • для слежения за массивами используйте спред-оператор, а для слежения за объектами — функцию toRefs().

Спасибо за внимание! Другие наши материалы на Habr о Vue:

Хочу перемен: почему пора переходить на Vue 3

Настройка ESLint для чистого кода в проектах на Vue

Подписывайся на наши соцсети! Авторские материалы для frontend-разработчиков мы также публикуем в ВК и Telegram.

Комментарии (5)


  1. ionicman
    08.11.2022 10:38
    +2

    Я правильно понимаю, что watch с immdiate и deep куда универсальней watchEffect?

    Хотя-бы потому, что не нужна пляска с бубном с развертыванием отслежиемого объекта, если нужно следить за изменением внутри объекта (как в примере массив), мало того, используя $watch его можно точно также остановить через unwatch, плюсом можно получить предыдущее состояние объетка.

    По-этому мне все еще не понятно зачем нужен watchEffect…


    1. SimbirSoft_frontend Автор
      08.11.2022 13:09

      У watchEffect есть свои плюсы, например, он позволяет отслеживать сразу несколько переменных. Кроме того, благодаря свойствам flush, onTrack, onTrigger можно оптимизировать некоторые процессы вычислений, а также watchEffect предоставляет удобный метод для отмены асинхронных операций.


      1. ionicman
        08.11.2022 16:48

        watch также может отслеживать несколько переменных, например через computed, или комплексную переменную

        Ну вот разве что события, опять-же этого можно добиться в watch через nextTick нет?


        1. SimbirSoft_frontend Автор
          10.11.2022 10:48

          Данный способ очень подробно описан в документации. Можно там посмотреть, как это правильно делается. На наш взгляд, способ слишком «костыльный», чтобы применять его на практике. Человек, который не знает этот нюанс или забыл, может не понять, что там происходит. WatchEffect позволяет сделать это более аккуратно, понятно и с меньшим количеством кода.


  1. freepad
    09.11.2022 21:39

    У вас написано:

    WatchEffect может отслеживать только адрес памяти реактивного объекта. Изменение элементов массива или свойств объекта не изменит адрес памяти и, следовательно, не вызовет срабатывания метода watchEffect

    Но после вы приводите примеры, где всё же изменение элементов массива или свойств объекта приводит к срабатыванию watchEffect. Получается всё же может отслеживать, но есть нюансы ;)

    для слежения за массивами используйте спред-оператор, а для слежения за объектами — функцию toRefs().

    Для объектов так же можно использовать (спред-оператор)[https://github.com/vuejs/core/issues/1351], НО! изменения будут отслеживаться поверхностно, как и в массивах на самом деле.

    То есть если у вас массив:

    const items = reactive([{ count: 0 }, { count: 10 }])
    watchEffect(() => {
        console.log(...items)
    })
    items.push({ count: 15 }) // watchEffect сработает
    items[0].count = 100 // watchEffect не сработает
    

    Чтобы для такого массива watchEffect работал по аналогии с watch c флагом deep, нам нужно создать копию объекта и каждое свойство у него дёрнуть, что добавит в watchEffect отслеживание всех свойств. Если костылить, то можно дёрнуть cloneDeep из lodash :)