Как очистить глобальные слушатели событий, интервалы и сторонние библиотеки в Vue-компонентах

от автора

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


ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/654875/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *