В идеале Vue-компоненты представляют собой самодостаточные части пользовательского интерфейса без каких-либо заметных побочных эффектов для объектов за пределами области действия данного элемента. Но, к сожалению, это не всегда возможно. Например, иногда нам нужно связать глобальных слушателей событий, использовать setInterval или инициализировать определенную стороннюю библиотеку внутри компонента.
// Vue 3
export default defineComponent({
name: 'SomeComponent',
setup() {
// Global event listener
document.body.addEventListener('click', () => {
// do something expensive ...
}, { capture: true });
// Interval
setInterval(() => {
// do something expensive ...
}, 2000);
// Third-party library
let flatpickrElement = ref(null);
onMounted(() => {
flatpickr(flatpickrElement.value);
});
// ...
},
});
// Vue 2
export default {
name: 'SomeComponent',
created() {
// Global event listener
document.body.addEventListener('click', () => {
// do something expensive ...
}, { capture: true });
// Interval
setInterval(() => {
// do something expensive ...
}, 2000);
},
mounted() {
// Third-party library
flatpickr(this.$refs.flatpickrElement);
},
};
В подобных случаях компоненты должны очищаться после их уничтожения. Если этого не сделать, могут произойти самые разнообразные неприятности — от сбоев в работе наших приложений до утечек памяти.
Удаление глобальных слушателей событий, очистка интервалов и сторонних библиотек
Давайте обновим наш демонстрационный пример, добавив хуки для очистки глобальных побочных эффектов, вызываемых нашим компонентом.
// Vue 3
export default defineComponent({
name: 'SomeComponent',
setup() {
// Global event listener
let options = { capture: true };
let callback = () => {
// do something expensive ...
};
document.body.addEventListener('click', callback, options);
onUnmounted(() => document.body.removeEventListener('click', callback, options));
// Interval
let intervalId = setInterval(() => {
// do something expensive ...
}, 2000);
onUnmounted(() => clearInterval(intervalId));
// Third-party library
let flatpickrElement = ref(null);
let flatpickrInstance;
onMounted(() => {
flatpickrInstance = flatpickr(flatpickrElement.value);
});
onUnmounted(() => flatpickrInstance.destroy());
// ...
},
});
// Vue 2
export default {
name: 'SomeComponent',
created() {
// Global event listener
let options = { capture: true };
let callback = () => {
// do something expensive ...
};
document.body.addEventListener('click', callback, options);
this.$once('hook:beforeDestroy', () => document.body.removeEventListener('click', callback, options));
// Interval
let intervalId = setInterval(() => {
// do something expensive ...
}, 2000);
this.$once('hook:beforeDestroy', () => clearInterval(intervalId));
// Third-party library
let flatpickrInstance;
this.$once('hook:mounted', () => {
flatpickrInstance = flatpickr(this.$refs.flatpickrElement);
});
this.$once('hook:beforeDestroy', () => flatpickrInstance.destroy());
},
};
Каждый раз, когда наш компонент уничтожается, вызываются соответствующие хуки, а глобальные побочные эффекты очищаются. Таким образом, нам не нужно беспокоиться об утечке памяти или о том, что глобальные слушатели событий будут накапливаться с каждым новым созданным нами инстансом компонента.
Хуки beforeDestroy и onUnmounted в тестах @vue/test-utils
Один из моих коллег обнаружил, что при тестировании компонентов с помощью замечательного пакета @vue/test-utils хуки beforeDestroy
и onUnmounted
не вызываются после теста! Так и задумано, хотя я не предполагал этого. В большинстве случаев это не проблема, но в иногда такое может привести к неожиданному поведению, когда тест-кейсы будут мешать друг другу из-за загрязненной глобальной области видимости.
test('It should make magic happen.', () => {
const wrapper = mount(SomeComponent);
// ...
expect(magicHappened).toBe(true);
// Vue 3.
wrapper.unmount();
// Vue 2.
wrapper.destroy();
});
Использование столь простого решения в тех редких случаях, когда это фактор, обычно нормально. Но люди могут запросто забыть об этом, поэтому я предпочитаю более общее решение.
Я считаю лучшей практикой обертывание сторонних зависимостей, и @vue/test-utils
не является исключением. Это позволяет нам установить параметры по умолчанию, которые целесообразно использовать для нашего приложения в глобальном масштабе.
// Vue 3
// test/utils.js
import { merge } from 'lodash';
import {
mount as vueTestUtilsMount,
} from '@vue/test-utils';
let defaultOptions = {
global: {
mocks: {
// Mocked plugins
$t: input => input,
},
},
// ...
};
export function mount(component, customOptions = {}) {
let options = merge({}, defaultOptions, customOptions);
return vueTestUtilsMount(component, options);
}
Более того, наличие кастомного модуля-обертки для @vue/test-utils
дает нам идеальное место для настройки глобального поведения, подобного этому. К счастью, в @vue/test-utils
для Vue 2 есть встроенная хелпер-функция, которая позволяет очень просто вызвать хук beforeDestroy
для каждого компонента, инициализированного во время тестирования.
// Vue 2
// test/utils.js
import { merge } from 'lodash';
import {
mount as vueTestUtilsMount,
enableAutoDestroy,
} from '@vue/test-utils';
// See: https://vue-test-utils.vuejs.org/api/#enableautodestroy-hook
enableAutoDestroy(afterEach);
let defaultOptions = {
mocks: {
// Mocked plugins
$t: input => input,
},
// ...
};
export function mount(component, customOptions = {}) {
let options = merge({}, defaultOptions, customOptions);
return vueTestUtilsMount(component, options);
}
К сожалению, этот хук, похоже, был удален в @vue/test-utils
для Vue 3. Поэтому нам нужно имплементировать данную функциональность самостоятельно.
// Vue 3
// test/utils.js
import { merge } from 'lodash';
import {
mount as vueTestUtilsMount,
} from '@vue/test-utils';
let defaultOptions = {
global: {
mocks: {
// Mocked plugins
$t: input => input,
},
},
// ...
};
let wrappers = new Set();
afterEach(() => {
wrappers.forEach(wrapper => wrapper.unmount());
wrappers.clear();
});
export function mount(component, customOptions = {}) {
let options = merge({}, defaultOptions, customOptions);
let wrapper = vueTestUtilsMount(component, options);
wrappers.add(wrapper);
return wrapper;
}
Подведение итогов
То, что сейчас переживает человечество в глобальном масштабе, справедливо и для программирования. Если устроить бардак, не взяв на себя ответственность за уборку после себя, это приведет к плохим последствиям.
В любом приложении среднего размера разработчик сталкивается с задачей централизованного управления стейтом.В современном Vue 3 мы можем это делать и без Vuex, полагаясь только на hooks + provide/inject. Приглашаем на открытый урок «Сравнение стейт менеджеров — Redux vs Vuex vs новый — Pinua», на котором рассмотрим плюсы и минусы такого подхода в реальном приложении.Также в сообществе широко обсуждается упрощённый стейт-менеджер под названием Pinya. На занятии установим его и научимся пользоваться. Регистрация для всех желающих доступна по ссылке.
Aidar87
Не чище ли выносить в beforeDestroy: function () {}