Олег Марков
Разрушение компонента во Vue - beforeDestroy и beforeUnmount
Введение
Разрушение компонента во Vue – это этап жизненного цикла, на котором фреймворк удаляет компонент со страницы, отцепляет его от дерева компонентов, очищает реактивные связи и DOM.
Хук beforeDestroy (во Vue 2) и его аналог beforeUnmount (во Vue 3) позволяют вам выполнить собственный код непосредственно перед тем, как компонент будет уничтожен. Здесь удобно отписываться от событий, очищать таймеры, закрывать соединения, освобождать ссылки на объекты и избегать утечек памяти.
В этой статье я покажу вам, как и когда срабатывает beforeDestroy, что в нем можно и что нельзя делать, как правильно освобождать ресурсы, чем отличается поведение во Vue 2 и Vue 3, и какие типичные ошибки допускаются при работе с этим хуком.
Жизненный цикл компонента и место хуков разрушения
Основные этапы жизненного цикла
Чтобы лучше понять beforeDestroy, полезно посмотреть на всю цепочку жизненного цикла компонента Vue 2:
- Инициализация:
- beforeCreate
- created
- Монтирование:
- beforeMount
- mounted
- Обновление:
- beforeUpdate
- updated
- Разрушение:
- beforeDestroy
- destroyed
Во Vue 3 названия финальных хуков слегка изменились:
- beforeUnmount – аналог beforeDestroy
- unmounted – аналог destroyed
Смотрите, я покажу вам на примере, где находятся хуки разрушения в последовательности:
export default {
data() {
return {
count: 0
}
},
beforeCreate() {
// 1. Компонент только создается, реактивности еще нет
console.log('beforeCreate')
},
created() {
// 2. Данные уже реактивны, но компонент еще не в DOM
console.log('created')
},
beforeMount() {
// 3. Перед первым рендером в DOM
console.log('beforeMount')
},
mounted() {
// 4. Компонент вставлен в DOM
console.log('mounted')
},
beforeUpdate() {
// 5. Перед повторным рендером (при изменении данных)
console.log('beforeUpdate')
},
updated() {
// 6. После перерисовки DOM
console.log('updated')
},
beforeDestroy() {
// 7. Прямо перед разрушением компонента
console.log('beforeDestroy')
},
destroyed() {
// 8. После разрушения компонента
console.log('destroyed')
}
}
Как видите, beforeDestroy – это последняя точка, в которой у вас еще есть доступ к:
- реактивным данным компонента
- его методам
- DOM-элементам через this.$el
- дочерним компонентам через this.$children
После destroyed компонент уже “мертв”, и опираться на его состояние не стоит.
Когда вызывается beforeDestroy
Причины разрушения компонента
Компонент может быть разрушен по разным причинам. Давайте разберем типовые случаи.
1. Условный рендеринг v-if
Если вы управляете показом компонента через v-if, его создание и разрушение будут происходить каждый раз при смене условия:
<template>
<div>
<button @click="show = !show">
Переключить компонент
</button>
<!-- Компонент создается и уничтожается -->
<child-component v-if="show" />
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
components: { ChildComponent },
data() {
return {
show: true
}
}
}
</script>
Внутри ChildComponent хук beforeDestroy сработает каждый раз, когда show станет false, а destroyed – когда Vue завершит разрушение.
2. Переключение маршрутов (Vue Router)
При смене маршрута компоненты, связанные с предыдущим маршрутом, будут разрушены:
<template>
<router-view />
</template>
Каждый компонент, который “уходит” со страницы при смене маршрута, вызовет свои beforeDestroy и destroyed. Это особенно важно, если вы, например, подписываетесь на WebSocket или слушаете события браузера (scroll, resize).
3. Динамическое создание/удаление компонентов
Если вы рендерите список компонентов по массиву и изменяете этот массив, “ушедшие” элементы будут разрушены:
<template>
<div>
<child-item
v-for="item in items"
:key="item.id"
:data="item"
/>
</div>
</template>
При удалении элемента из items соответствующий child-item вызовет beforeDestroy.
Что можно и нужно делать в beforeDestroy
Основные задачи beforeDestroy
Главный смысл beforeDestroy – очистка и освобождение ресурсов. Давайте перечислим основные задачи, которые логично выполнять в этом хуке:
- Отписка от глобальных слушателей событий:
- window.addEventListener
- document.addEventListener
- this.$root.$on / this.$bus.$on (при использовании шины событий)
- Очистка таймеров:
- clearInterval
- clearTimeout
- отмена requestAnimationFrame
- Закрытие внешних соединений:
- WebSocket
- SSE (EventSource)
- сторонние библиотеки, подписки и т.п.
- Освобождение ссылок на большие объекты:
- кэшированные данные в полях компонента
- ссылки на DOM-элементы, созданные вручную
Давайте разберемся на примерах.
Пример 1. Очистка таймера setInterval
export default {
data() {
return {
count: 0,
intervalId: null // Здесь храним идентификатор таймера
}
},
mounted() {
// Здесь я запускаю таймер, который каждую секунду увеличивает счетчик
this.intervalId = setInterval(() => {
this.count++
}, 1000)
},
beforeDestroy() {
// Очень важно очистить таймер перед разрушением
if (this.intervalId !== null) {
clearInterval(this.intervalId) // Останавливаем таймер
this.intervalId = null // Обнуляем ссылку
}
}
}
Если этого не сделать, таймер продолжит работать даже после уничтожения компонента, что может привести к утечке памяти и странному поведению.
Пример 2. Отписка от событий окна
export default {
data() {
return {
width: window.innerWidth
}
},
mounted() {
// Добавляем слушатель изменения размера окна
window.addEventListener('resize', this.handleResize)
},
methods: {
handleResize() {
// Обновляем ширину при изменении размера окна
this.width = window.innerWidth
}
},
beforeDestroy() {
// Обязательно убираем слушатель
window.removeEventListener('resize', this.handleResize)
}
}
Обратите внимание, что мы передаем в removeEventListener ровно ту же функцию (this.handleResize), которую добавляли. Если использовать анонимную функцию, снять такой обработчик вы не сможете.
Пример 3. Очистка WebSocket соединения
export default {
data() {
return {
socket: null, // Здесь будет храниться объект WebSocket
messages: []
}
},
mounted() {
// Здесь я создаю WebSocket соединение и сохраняю ссылку
this.socket = new WebSocket('wss://example.com/stream')
// Подписываемся на входящие сообщения
this.socket.addEventListener('message', this.handleMessage)
// Можно также обработать открытие и ошибки
this.socket.addEventListener('open', () => {
console.log('WebSocket открыт')
})
},
methods: {
handleMessage(event) {
// Парсим входящее сообщение
const data = JSON.parse(event.data)
this.messages.push(data)
}
},
beforeDestroy() {
if (this.socket) {
// Убираем слушатель
this.socket.removeEventListener('message', this.handleMessage)
// Закрываем соединение
this.socket.close()
// Освобождаем ссылку
this.socket = null
}
}
}
Так вы избежите ситуации, когда сервер продолжает слать данные в несуществующий уже компонент.
Что делать в destroyed и чем он отличается от beforeDestroy
beforeDestroy vs destroyed
Смотрите, разница довольно проста:
- beforeDestroy – вызывается до начала процесса разрушения
- destroyed – вызывается после того, как:
- все дочерние компоненты уничтожены
- наблюдатели (watchers) отцеплены
- связи с реактивной системой удалены
Практическая разница:
- В beforeDestroy:
- все еще можно безопасно обращаться к данным
- можно общаться с родителем или дочерними компонентами
- можно снимать подписки, пока все “живое”
- В destroyed:
- компонент уже де-факто отключен от реактивной системы
- DOM-элемент уже удален (или вот-вот будет)
- лучше не пытаться обновлять данные или ждать реактивных изменений
Чаще всего вам достаточно одного beforeDestroy. destroyed нужен реже, например, для логирования факта окончательного разрушения или интеграции с системами мониторинга.
Разрушение компонента и Vue 3 beforeUnmount
Переименование хуков
Во Vue 3 семейство хуков, связанных с разрушением, выглядит так:
- beforeUnmount – аналог beforeDestroy
- unmounted – аналог destroyed
Если вы пишете компонент во Vue 3 в опциональном API, код будет сильно похож:
export default {
data() {
return {
intervalId: null
}
},
mounted() {
this.intervalId = setInterval(() => {
console.log('tick')
}, 1000)
},
beforeUnmount() {
// Здесь я очищаю таймер перед размонтированием
if (this.intervalId !== null) {
clearInterval(this.intervalId)
this.intervalId = null
}
},
unmounted() {
console.log('Компонент окончательно размонтирован')
}
}
Composition API и хуки onBeforeUnmount
В Composition API (Vue 3) вы используете специальные функции-хуки:
- onBeforeUnmount – аналог beforeUnmount
- onUnmounted – аналог unmounted
Давайте посмотрим, что происходит в следующем примере:
import { ref, onMounted, onBeforeUnmount } from 'vue'
export default {
setup() {
const count = ref(0)
let intervalId = null // Переменная в области setup
onMounted(() => {
// Запускаем таймер после монтирования
intervalId = setInterval(() => {
count.value++
}, 1000)
})
onBeforeUnmount(() => {
// Чистим таймер перед размонтированием
if (intervalId !== null) {
clearInterval(intervalId)
intervalId = null
}
})
// Возвращаем реактивные данные в шаблон
return { count }
}
}
Обратите внимание:
- Мы не используем this, а работаем с переменными в области видимости setup
- Логика по очистке (таймер, подписки) такая же, как в Vue 2, меняется только синтаксис
Разрушение дочерних компонентов и порядок вызова хуков
Порядок разрушения дерева компонентов
Если у вас есть родительский и дочерний компоненты, при разрушении родителя Vue будет следовать такому порядку:
- Вызывается beforeDestroy у дочерних компонентов
- Дочерние компоненты проходят полный свой цикл разрушения (beforeDestroy → destroyed)
- После этого вызывается beforeDestroy у родителя
- Затем destroyed у родителя
Давайте посмотрим на такой пример:
// Child.vue
export default {
name: 'ChildComponent',
beforeDestroy() {
console.log('Child beforeDestroy')
},
destroyed() {
console.log('Child destroyed')
}
}
// Parent.vue
import ChildComponent from './Child.vue'
export default {
components: { ChildComponent },
template: `
<div>
<child-component />
</div>
`,
beforeDestroy() {
console.log('Parent beforeDestroy')
},
destroyed() {
console.log('Parent destroyed')
}
}
Лог в консоли при уничтожении родителя будет таким:
- Child beforeDestroy
- Child destroyed
- Parent beforeDestroy
- Parent destroyed
Это важно, если вы, например, хотите в родителе взаимодействовать с дочерним компонентом в момент разрушения. В beforeDestroy родителя дочерние компоненты уже уничтожены, поэтому обращаться к ним уже нельзя.
beforeDestroy и хранение состояния вне компонента
Сторонние состояния и утечки памяти
Частая ситуация: вы храните части состояния вне Vue, например:
- в глобальных структурах данных
- в сторонних библиотеках
- в event bus (шина событий)
Если при разрушении компонента не “отцепить” его от этих внешних структур, ссылка на компонент может сохраниться где-то снаружи. Это приведет к тому, что сборщик мусора не освободит память, и вы получите утечки.
Давайте посмотрим, как это может выглядеть.
Пример с шиной событий
// eventBus.js
import Vue from 'vue'
export const eventBus = new Vue()
// Component.vue
import { eventBus } from './eventBus'
export default {
data() {
return {
message: ''
}
},
created() {
// Подписываемся на событие
eventBus.$on('update-message', this.handleUpdate)
},
methods: {
handleUpdate(newMessage) {
this.message = newMessage
}
},
beforeDestroy() {
// Отписываемся от события перед разрушением
eventBus.$off('update-message', this.handleUpdate)
}
}
Если забыть про $off, объект компонента может остаться “живым” через ссылку в $on, и это уже шаг к утечке памяти.
Что нельзя делать в beforeDestroy
Потенциально опасные действия
В beforeDestroy, несмотря на то, что компонент еще “живой”, стоит избегать некоторых действий.
1. Не инициировать долгие асинхронные операции
Например, не начинайте длительные HTTP-запросы, которые должен был бы обрабатывать уже несуществующий компонент. Если запрос жизненно необходим – выносите его в более высокоуровневый слой, например, в Vuex или внешний сервис.
2. Не изменять состояние, от которого зависят другие компоненты без необходимости
Технически вы можете менять данные в beforeDestroy, но важно понимать, что некоторые части реактивной цепочки уже могут быть на пути к разрушению. В редких случаях это может вести к неожиданному порядку обновлений.
3. Не полагаться на то, что beforeDestroy всегда будет вызван
В обычной работе приложения Vue гарантирует вызов beforeDestroy при корректном уничтожении компонентов. Но в теории, если страница внезапно перезагрузится или вкладка закроется, ваш код может просто не успеть выполниться. Поэтому хук нельзя использовать как единственный способ “гарантированного” сохранения данных.
Альтернативы разрушению компонента – v-if против v-show
Когда компонент не разрушается
Важно не путать ситуации, когда компонент скрывается, но не уничтожается.
- v-if – создает и разрушает компонент при смене условия
- v-show – просто меняет CSS-свойство display, компонент остается в памяти
Смотрите, я покажу вам разницу:
<!-- Здесь компонент будет создаваться и уничтожаться -->
<child-component v-if="visible" />
<!-- Здесь компонент создается один раз и только скрывается -->
<child-component v-show="visible" />
В случае v-show хуки beforeDestroy и destroyed не вызываются при скрытии компонента. Он продолжает существовать, просто его элемент скрыт. Это полезно, когда:
- нужно быстро показывать/скрывать компонент
- создание компонента дорогое по ресурсам
- важно сохранить состояние между показами
Но если ваша цель – именно освободить ресурсы и уничтожить компонент, используйте v-if.
Практические паттерны использования beforeDestroy
Паттерн 1. “Подписался – от подпишись” (subscribe–unsubscribe)
Удобно придерживаться правила: если вы где-то в компоненте “подписались” или “создали”, то в beforeDestroy вы обязательно “отпишитесь” или “уничтожьте”.
Типичные пары:
- mounted / created → beforeDestroy
- onMounted → onBeforeUnmount (Vue 3)
Пример стандартного паттерна:
export default {
data() {
return {
intervalId: null,
listener: null
}
},
mounted() {
// Создаем слушателя
this.listener = (event) => {
console.log('scroll', window.scrollY)
}
// Подписываемся на событие
window.addEventListener('scroll', this.listener)
// Создаем таймер
this.intervalId = setInterval(() => {
console.log('tick')
}, 1000)
},
beforeDestroy() {
// Снимаем слушатель, если он есть
if (this.listener) {
window.removeEventListener('scroll', this.listener)
this.listener = null
}
// Чистим таймер
if (this.intervalId !== null) {
clearInterval(this.intervalId)
this.intervalId = null
}
}
}
Паттерн 2. Обертка над ресурсом
Иногда удобно вынести работу с ресурсом в отдельный объект-обертку, а в beforeDestroy просто вызвать метод cleanup.
// resourceManager.js
export class ResourceManager {
constructor() {
this.intervalId = null
}
start() {
// Здесь я запускаю некий периодический процесс
this.intervalId = setInterval(() => {
console.log('ResourceManager tick')
}, 1000)
}
cleanup() {
// Очищаем ресурсы
if (this.intervalId !== null) {
clearInterval(this.intervalId)
this.intervalId = null
}
}
}
// Component.vue
import { ResourceManager } from './resourceManager'
export default {
data() {
return {
manager: null
}
},
created() {
// Инициализируем менеджер
this.manager = new ResourceManager()
this.manager.start()
},
beforeDestroy() {
// Перед разрушением компонента чистим ресурсы менеджера
if (this.manager) {
this.manager.cleanup()
this.manager = null
}
}
}
Так код очистки становится более структурированным и легко переиспользуемым.
Заключение
Хук beforeDestroy во Vue 2 (и его аналог beforeUnmount во Vue 3) – ключевое место для освобождения ресурсов компонента: отписки от событий, остановки таймеров, закрытия соединений и очистки ссылок на тяжелые объекты. Это последний момент, когда компонент еще полностью работоспособен и у вас есть доступ ко всем его данным и методам.
Грамотное использование beforeDestroy помогает:
- избегать утечек памяти
- предотвращать работу “висячих” слушателей и таймеров после ухода компонента со страницы
- аккуратно завершать работу внешних ресурсов
При разработке удобно придерживаться простого правила: если вы что-то создаете или на что-то подписываетесь в created или mounted, обязательно предусмотрите симметричную очистку в beforeDestroy (или beforeUnmount / onBeforeUnmount в Vue 3).
Также важно различать случаи, когда компонент действительно уничтожается (v-if, смена роутера), и когда он только скрывается (v-show). beforeDestroy вызывается только в первом случае.
Если вы выстраиваете логику приложения вокруг жизненного цикла компонентов, внимательно относитесь к фазе разрушения – это делает поведение приложения стабильным и предсказуемым.
Частозадаваемые технические вопросы по теме и ответы
Как правильно использовать beforeDestroy в миксинах и что если несколько миксинов тоже объявляют этот хук?
Во Vue 2 хуки из миксинов не перезаписывают друг друга, а вызываются все по очереди. То есть если компонент и несколько миксинов объявляют beforeDestroy, при разрушении будут вызваны все эти функции.
Мини-инструкция:
- В миксине объявляйте beforeDestroy только для очистки тех ресурсов, которые миксин сам и создает.
- В компоненте используйте beforeDestroy для очистки компонент-специфичных ресурсов.
- Не полагайтесь на порядок вызова хуков между миксинами, лучше делайте их независимыми друг от друга.
Как отследить, что компонент точно уничтожился и не остались “живые” подписки?
Подход:
- В beforeDestroy логируйте ключевую информацию (например, идентификаторы таймеров, наличие подписок).
- В destroyed (или unmounted) добавьте финальный лог.
- Включите Vue devtools и наблюдайте дерево компонентов – уничтоженные компоненты не должны оставаться в дереве.
- Для сложных случаев используйте инструмент профилирования памяти браузера и проверяйте, не остаются ли объекты компонентов в heap snapshot.
Почему мой обработчик события все еще срабатывает после разрушения компонента?
Чаще всего вы забыли отписаться от события или отписываетесь не той ссылкой на функцию.
Мини-инструкция:
- Никогда не используйте анонимные функции в addEventListener, если собираетесь отписываться.
- Сохраните ссылку на обработчик в методе компонента или отдельной переменной.
- В beforeDestroy вызовите removeEventListener с точно той же функцией.
- Если используете стороннюю библиотеку событий, изучите ее метод отписки (off, unsubscribe и т.п.) и вызывайте его в хуке разрушения.
Можно ли вызывать this.$destroy() вручную и как это связано с beforeDestroy?
Во Vue 2 можно уничтожить компонент вручную через this.$destroy().
При этом:
- Vue вызовет beforeDestroy и destroyed так же, как при “естественном” разрушении.
- DOM-элемент компонента будет удален, но ссылка в родителе (если он все еще существует) может остаться, поэтому ручное уничтожение стоит использовать осторожно.
- Если вы создаете компонент программно через new Vue или render динамических корней, обязательно вызывайте $destroy() и дополнительно удаляйте DOM-узел, если Vue этого не делает сам.
Как правильно тестировать логику очистки в beforeDestroy во Vue 2 и onBeforeUnmount во Vue 3?
Подход для unit-тестов:
- Смонтируйте компонент с помощью @vue/test-utils.
- Создайте/запустите ресурсы (таймеры, подписки) в mounted.
- В тесте вызовите wrapper.destroy() (Vue 2) или wrapper.unmount() (Vue 3).
- Проверьте, что методы очистки были вызваны:
- с помощью spy (jest.spyOn, sinon.spy)
- или проверкой побочных эффектов (например, что clearInterval был вызван с нужным id).
- Для Composition API проверяйте, что onBeforeUnmount срабатывает при размонтировании так же, как beforeDestroy.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев