
В идеале 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. На занятии установим его и научимся пользоваться. Регистрация для всех желающих доступна по ссылке.
