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 — Оба урока про внутреннее устройство реактивности, которое обычно скрыто от прикладного кода