Vue

effectScope и управление эффектами

Композабл useFeed заводит три watch и один watchEffect, отслеживающих фильтры, скролл и автообновление. Внутри компонента это работает: при размонтировании Vue сам останавливает эффекты, привязанные к экземпляру. Но как только тот же useFeed вызывается вне компонента - в обычной функции, в Pinia-сторе, в синглтоне - привязки к компоненту нет, и эффекты живут вечно, продолжая реагировать на изменения после того, как стали ненужными. effectScope собирает все эти эффекты в одну область и останавливает их одним вызовом scope.stop().

  • Pinia: каждый стор живёт в собственном effectScope, и его getters-computed корректно освобождаются при остановке стора
  • Композаблы, вызываемые вне компонента (в сторах, плагинах, синглтонах), где автоочистка по unmount недоступна
  • Переиспользуемые модули состояния, которые нужно создавать и уничтожать вручную по требованию
  • Библиотеки реактивных утилит (VueUse), создающие группы эффектов с единой точкой очистки
  • Тесты реактивной логики, где после проверки нужно гарантированно остановить все созданные watch

Предварительные знания

  • Понимание эффектов: watch, watchEffect и computed как реактивные побочные эффекты
  • Знание, что watchEffect и watch возвращают функцию остановки
  • Идея жизненного цикла компонента и автоочистки эффектов при размонтировании

Зачем группировать эффекты

Каждый watch, watchEffect и computed создаёт реактивный эффект - подписку, которая срабатывает при изменении зависимостей. Внутри компонента у этих эффектов есть владелец: при размонтировании Vue останавливает их сам. Но если эффект создан вне компонента, владельца нет, и эффект продолжает жить и реагировать на изменения, пока на него остаётся ссылка. Это утечка: подписки накапливаются, колбэки срабатывают зря.

Можно собирать функции остановки вручную: watch возвращает stop, складываем их в массив и вызываем по одной. Но при десятке эффектов и вложенных композаблах это превращается в бухгалтерию ссылок. effectScope решает задачу системно: один контейнер на все эффекты внутри него.

Правило простое: пока эффект создаётся внутри setup компонента, об очистке думать не нужно. Вопрос встаёт только когда реактивная логика живёт вне компонента и создаётся или уничтожается по собственному жизненному циклу.

Почему watch, созданный внутри компонента, не нужно останавливать вручную, а вне компонента нужно?

effectScope и onScopeDispose

effectScope() создаёт область. Метод scope.run(fn) выполняет функцию, и все эффекты, запущенные внутри неё, захватываются областью. После этого один вызов scope.stop() останавливает их все сразу. Это превращает группу эффектов в единый управляемый объект с понятным концом жизни.

onScopeDispose регистрирует колбэк очистки внутри текущей области. Когда область останавливается, колбэк выполняется. Это аналог onUnmounted, но привязанный не к компоненту, а к области, поэтому работает и в композаблах, вызванных вне компонента. Туда кладут отписку от внешних подписок: таймеры, слушатели событий, сокеты.

effectScope(true) создаёт отдельную область, не привязанную к родительской. По умолчанию вложенные области образуют дерево: остановка родителя останавливает и детей. Отдельная область нужна, когда жизненный цикл группы эффектов независим от внешней области.

Что делает scope.stop() после того, как внутри scope.run() созданы три watch?

Как effectScope используют композаблы и Pinia

effectScope - низкоуровневый примитив для авторов библиотек и переиспользуемой логики. Прикладной код вызывает его редко напрямую, но опирается на него постоянно. Pinia заворачивает каждый стор в собственный effectScope: getters-computed и watch внутри стора захватываются областью, и когда стор останавливается (например, при HMR или ручном `scope.stop()`), все его эффекты освобождаются разом, без утечек.

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

Практический вывод для прикладного разработчика: effectScope нужен, когда пишется переиспользуемая реактивная логика, живущая вне одного компонента. В обычных компонентах автоочистка по размонтированию покрывает всё, и тянуться к effectScope не нужно.

Зачем Pinia оборачивает каждый стор в собственный effectScope?

Связь с другими темами

Урок раскрывает механизм, на котором стоят композаблы и Pinia:

  • Эффекты и реактивность — effectScope управляет жизненным циклом именно реактивных эффектов
  • Pinia — Каждый стор Pinia обёрнут в effectScope для корректной остановки эффектов
  • Движок alien-signals — Ещё одна тема про внутреннее устройство реактивности, скрытое от прикладного кода

Итог

  • Эффекты, созданные внутри компонента, останавливаются автоматически при его размонтировании, но вне компонента такой привязки нет
  • effectScope создаёт область, которая захватывает все эффекты (watch, watchEffect, computed), запущенные внутри её функции
  • Вызов scope.stop() останавливает сразу все захваченные эффекты, освобождая подписки одним действием
  • onScopeDispose регистрирует колбэк очистки, который выполнится при остановке текущей области, как onUnmounted для компонента
  • Композаблы и Pinia используют effectScope под капотом, чтобы управлять группами эффектов вне привязки к компоненту

Связанные уроки

  • vue-18-reactivity-deep — effectScope управляет именно эффектами (watch, computed, watchEffect), поэтому нужно понимать их природу
  • vue-27-pinia-intro — Pinia держит каждый стор внутри собственного effectScope, чтобы корректно останавливать его эффекты
  • vue-23-alien-signals — Оба урока про внутреннее устройство реактивности, которое обычно скрыто от прикладного кода
effectScope и управление эффектами

0

1

Войти