Джедайские приемы на JavaScript: магические свойства транслятора событий

О чем мы?

Event Emitter можно перевести как “транслятор” или “эмиттер” событий. Звучит как название штуки, умеющей генерировать событие, которое может “услышать” кто угодно.

Представьте себе такую схему: в вашем асинхронном коде определенный участок может “крикнуть” остальным, что он выполнил свою задачу, а другие части “услышат” этот сигнал и примут соответствующие меры.

Event Emitter — это шаблон, который можно реализовать разными способами. Основная идея в том, чтобы грамотно создать основу для управления событиями и реализовать возможность любым элементам “подписаться” на него (и быть в курсе происходящего). С другими шаблонами проектирования вы можете познакомиться в нашей статье.

Уже интересно, как такая магия может работать? Итак, мы хотим добиться кода, который затем можно будет использовать так:

let input = document.querySelector('input[type="text"]');
let button = document.querySelector('button');
let h1 = document.querySelector('h1');

button.addEventListener('click', () => {
  emitter.emit('event:name-changed', {name: input.value});
});

let emitter = new EventEmitter();
emitter.subscribe('event:name-changed', data => {
  h1.innerHTML = `Your name is: ${data.name}`;
});

Начнем.

Реализация

class EventEmitter {
  constructor() {
    this.events = {};
  }
}

Как видите, конструктор нашего класса будет инициализировать поле events, пока что делая его пустым объектом. Задача этого поля — хранить события, “подписавшиеся” на нас (то есть в нем будут храниться функции).

Метод subscribe:

subscribe( eventName, fn ) {
  if( !this.events[eventName] ) {
     this.events[eventName] = [];
  }
    
  this.events[eventName].push(fn);
}

Этот метод принимает в качестве аргументов название события (например, event:name-changed, как в нашем примере) и функцию, которая будет вызываться, когда будет инициироваться транслируемое событие.

Одна из ключевых особенностей функций в JavaScript состоит в том, что функции — это этакие “объекты первого класса”, то есть мы можем передать функцию в качестве параметра другой функции, как в методе subscribe().

Метод emit:

emit(eventName, data) {
  const event = this.events[eventName];
  if( event ) {
    event.forEach(fn => {
       fn.call(null, data);
     });
   }
}

Этот метод принимает имя события, которое мы хотим всем транслировать, и данные, которые будут отправляться в момент этого события. Если в экземпляре класса сохранены какие-то подписанные на него события, мы проходимся по каждому из них и вызываем каждое, передавая ему данные, которые хотим транслировать.

Собственно, с реализацией паттерна мы закончили. Но пока остается одна проблема: нам нужно будет “отписать” функции, которые нам больше не нужны. Не сделаем этого — столкнемся с утечкой памяти.

Давайте решим эту проблему. Пусть метод subscribe() возвращает функцию unsubscribe(), которую позже можно будет использовать, чтобы отписаться от события.

subscribe(eventName, fn) {
  if(!this.events[eventName]) {
    this.events[eventName] = [];
  }
    
  this.events[eventName].push(fn);
  
  return () => {
    this.events[eventName] = this.events[eventName].filter(eventFn => fn !== eventFn);
  }
}

Как мы помним, в JavaScript особенные функции — их легко можно вернуть из другой функции. Теперь метод subscribe() можно использовать следующим образом:

let unsubscribe = emitter.subscribe('event:name-changed', data => console.log(data));

unsubscribe();

Вызывая сохраненную в переменной функцию unsubscribe(), мы отписываемся от события.

Вот и все. Пока-пока, утечки памяти!

Вот тут можно попробовать весь получившийся код в действии.

Источник: Medium