Обработка событий в Node.js с помощью EventEmitter

Введение В этом руководстве мы рассмотрим собственный класс Node EventEmitter. Вы узнаете о событиях, о том, что вы можете делать с EvenEmitter, и о том, как использовать события в своем приложении. Мы также рассмотрим, какие другие собственные модули расширяют класс EventEmitter, и рассмотрим несколько примеров, чтобы понять, что происходит за кулисами. Итак, вкратце, мы рассмотрим почти все, что вам нужно знать о классе EventEmitter. Мы будем использовать некоторые базовые функции ES6, такие как

Вступление

В этом руководстве мы рассмотрим собственный класс EventEmitter Вы узнаете о событиях, о том, что вы можете делать с EvenEmitter , и о том, как использовать события в своем приложении.

Мы также рассмотрим, какие другие собственные модули расширяют EventEmitter и рассмотрим несколько примеров, чтобы понять, что происходит за кулисами.

Итак, вкратце, мы рассмотрим почти все, что вам нужно знать о классе EventEmitter

В этом руководстве мы будем использовать некоторые базовые функции ES6, такие как классы JavaScript и стрелочные функции. Это полезно, но не обязательно, если вы уже знакомы с синтаксисом ES6.

Что такое событие?

Вся парадигма программного обеспечения вращается вокруг событий и их использования. Архитектура, управляемая событиями, довольно распространена в настоящее время, и приложения, управляемые событиями, создают, обнаруживают и реагируют на различные виды событий.

Можно утверждать, что ядро Node.js частично управляется событиями, поскольку многие собственные модули, такие как файловая система ( fs ) и stream модуль, сами написаны как EventEmitter .

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

Вы можете рассматривать программы, управляемые событиями, как модели публикации-подписки, в которых издатель инициирует события, а подписчики их слушают и действуют соответствующим образом.

Например, предположим, что у нас есть сервер изображений, на который пользователи могут загружать изображения. В программировании, управляемом событиями, такое действие, как загрузка изображения, вызывает событие. Чтобы воспользоваться этим, на это событие 1..n

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

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

Что такое EventEmitter?

Класс EventEmitter - это встроенный класс, который находится в модуле events По документации:

Большая часть API ядра Node.js построена на идиоматической асинхронной управляемой событиями архитектуре, в которой определенные виды объектов (называемые «эмиттерами») генерируют именованные события, которые вызывают Function объектов («слушателей») »

Этот класс можно до некоторой степени описать как вспомогательную реализацию модели pub / sub, поскольку он помогает источникам событий (издателям) публиковать события (сообщения), а слушателям (подписчикам) действовать в отношении этих событий - простым способом.

Создание EventEmitters

При этом давайте продолжим и создадим EventEmitter . Это можно сделать либо путем создания экземпляра самого класса, либо путем его реализации через настраиваемый класс и последующего создания экземпляра этого класса.

Создание объекта EventEmitter

Начнем с простого объекта, излучающего события. Мы создадим EventEmitter который каждую секунду будет генерировать событие, содержащее информацию о времени безотказной работы приложения.

Сначала импортируйте класс EventEmitter из модулей events

 const { EventEmitter } = require('events'); 

Затем создадим EventEmitter :

 const timerEventEmitter = new EventEmitter(); 

Опубликовать событие из этого объекта очень просто:

 timerEventEmitter.emit("update"); 

Мы указали название события и опубликовали его как событие. Однако ничего не происходит, поскольку нет слушателя, который бы реагировал на это событие. Сделаем так, чтобы это событие повторялось каждую секунду.

Используя метод setInterval() , создается таймер, который будет публиковать update каждую секунду:

 let currentTime = 0; 
 
 // This will trigger the update event each passing second 
 setInterval(() => { 
 currentTime++; 
 timerEventEmitter.emit('update', currentTime); 
 }, 1000); 

EventEmitter принимает имя события и произвольный набор аргументов. В этом случае мы передали eventName как update и currentTime как время от начала приложения.

Мы запускаем эмиттер с помощью emit() , который отправляет событие с предоставленной нами информацией.

Когда наш эмиттер событий готов, давайте подпишем на него слушателя событий:

 timerEventEmitter.on('update', (time) => { 
 console.log('Message Received from publisher'); 
 console.log(`${time} seconds passed since the program started`); 
 }); 

Использование on() передачей имени события, чтобы указать, к какому из них мы хотели бы прикрепить слушателя, позволяет нам создавать слушателей. При update запускается метод, который регистрирует время. Вы можете добавлять одного и того же слушателя снова и снова, и каждый из них будет подписываться на событие.

Второй аргумент функции on() - это обратный вызов, который может принимать любое количество дополнительных данных, которые были отправлены событием. Каждый слушатель может выбрать, какие данные ему нужны, после сохранения порядка.

Запуск этого скрипта должен дать:

 Message Received from publisher 
 1 seconds passed since the program started 
 Message Received from publisher 
 2 seconds passed since the program started 
 Message Received from publisher 
 3 seconds passed since the program started 
 ... 

Напротив, мы можем использовать метод once() для подписки - если вам нужно выполнить что-то только при первом срабатывании события:

 timerEventEmitter.once('update', (time) => { 
 console.log('Message Received from publisher'); 
 console.log(`${time} seconds passed since the program started`); 
 }); 

Запуск этого кода даст:

 Message Received from publisher 
 1 seconds passed since the program started 

EventEmitter с несколькими слушателями

Теперь давайте создадим другой тип генератора событий с тремя слушателями. Это будет обратный отсчет. Один слушатель будет обновлять пользователя каждую секунду, один слушатель будет уведомлять пользователя, когда обратный отсчет приближается к концу, и последний слушатель сработает, когда обратный отсчет закончится:

  • update - это событие будет срабатывать каждую секунду
  • end - это событие сработает по окончании обратного отсчета.
  • end-soon - это событие сработает за 2 секунды до окончания обратного отсчета.

Создадим функцию, которая создает этот эмиттер событий и возвращает его:

 const countDown = (countdownTime) => { 
 const eventEmitter = new EventEmitter(); 
 
 let currentTime = 0; 
 
 // This will trigger the update event each passing second 
 const timer = setInterval(() => { 
 currentTime++; 
 eventEmitter.emit('update', currentTime); 
 
 // Check if countdown has reached to the end 
 if (currentTime === countdownTime) { 
 clearInterval(timer); 
 eventEmitter.emit('end'); 
 } 
 
 // Check if countdown will end in 2 seconds 
 if (currentTime === countdownTime - 2) { 
 eventEmitter.emit('end-soon'); 
 } 
 }, 1000); 
 return eventEmitter; 
 }; 

В этой функции мы запустили событие на основе интервала, update

При первом if мы проверяем, достиг ли обратный отсчет конца, и останавливаем событие на основе интервала. Если это так, мы запускаем end событие.

Во втором условии мы проверяем, находится ли обратный отсчет через 2 секунды до окончания, и публикуем end-soon если это так.

Теперь давайте добавим несколько подписчиков к этому эмиттеру событий:

 const myCountDown = countDown(5); 
 
 myCountDown.on('update', (t) => { 
 console.log(`${t} seconds since the timer started`); 
 }); 
 
 myCountDown.on('end', () => { 
 console.log('Countdown is completed'); 
 }); 
 
 myCountDown.on('end-soon', () => { 
 console.log('Count down will end in 2 seconds'); 
 }); 

Этот код должен дать:

 1 seconds has been passed since the timer started 
 2 seconds has been passed since the timer started 
 3 seconds has been passed since the timer started 
 Count down will end in 2 seconds 
 4 seconds has been passed since the timer started 
 5 seconds has been passed since the timer started 
 Countdown is completed 

Расширение EventEmitter

В этом разделе давайте создадим эмиттер событий с той же функциональностью, расширив класс EventEmitter Сначала создайте CountDown который будет обрабатывать события:

 const { EventEmitter } = require('events'); 
 
 class CountDown extends EventEmitter { 
 constructor(countdownTime) { 
 super(); 
 this.countdownTime = countdownTime; 
 this.currentTime = 0; 
 } 
 
 startTimer() { 
 const timer = setInterval(() => { 
 this.currentTime++; 
 this.emit('update', this.currentTime); 
 
 // Check if countdown has reached to the end 
 if (this.currentTime === this.countdownTime) { 
 clearInterval(timer); 
 this.emit('end'); 
 } 
 
 // Check if countdown will end in 2 seconds 
 if (this.currentTime === this.countdownTime - 2) { 
 this.emit('end-soon'); 
 } 
 }, 1000); 
 } 
 } 

Как видите, мы можем использовать this.emit() напрямую внутри класса. Кроме того, startTimer() используется, чтобы мы могли контролировать, когда начинается обратный отсчет. В противном случае он начнется, как только объект будет создан.

Создадим новый объект CountDown и подпишемся на него:

 const myCountDown = new CountDown(5); 
 
 myCountDown.on('update', (t) => { 
 console.log(`${t} seconds has been passed since the timer started`); 
 }); 
 
 myCountDown.on('end', () => { 
 console.log('Countdown is completed'); 
 }); 
 
 myCountDown.on('end-soon', () => { 
 console.log('Count down will be end in 2 seconds'); 
 }); 
 
 myCountDown.startTimer(); 

Выполнение этого приведет к:

 1 seconds has been passed since the timer started 
 2 seconds has been passed since the timer started 
 3 seconds has been passed since the timer started 
 Count down will be end in 2 seconds 
 4 seconds has been passed since the timer started 
 5 seconds has been passed since the timer started 
 Countdown is completed 

Псевдоним для функции on() - addListener() . Рассмотрим прослушиватель событий " end-soon

 myCountDown.on('end-soon', () => { 
 console.log('Count down will be end in 2 seconds'); 
 }); 

Мы могли бы сделать то же самое с addListener() вот так:

 myCountDown.addListener('end-soon', () => { 
 console.log('Count down will be end in 2 seconds'); 
 }); 

Они оба работают. Они почти как синонимы. Однако большинство программистов предпочитают использовать on() .

Важные функции EventEmitter

Давайте посмотрим на некоторые важные функции, которые мы можем использовать в EventEmitter s.

eventNames ()

Эта функция вернет все имена активных слушателей в виде массива:

 const myCountDown = new CountDown(5); 
 
 myCountDown.on('update', (t) => { 
 console.log(`${t} seconds has been passed since the timer started`); 
 }); 
 
 myCountDown.on('end', () => { 
 console.log('Countdown is completed'); 
 }); 
 
 myCountDown.on('end-soon', () => { 
 console.log('Count down will be end in 2 seconds'); 
 }); 
 
 console.log(myCountDown.eventNames()); 

Выполнение этого кода приведет к:

 [ 'update', 'end', 'end-soon' ] 

Если бы мы подписались на другое событие, такое как myCount.on('some-event', ...) , новое событие также будет добавлено в массив.

Имейте в виду, что этот метод не возвращает опубликованные события. Он возвращает список событий, на которые подписаны.

removeListener ()

Как следует из названия, эта функция удаляет подписанный обработчик из EventEmitter :

 const { EventEmitter } = require('events'); 
 
 const emitter = new EventEmitter(); 
 
 const f1 = () => { 
 console.log('f1 Triggered'); 
 } 
 
 const f2 = () => { 
 console.log('f2 Triggered'); 
 } 
 
 emitter.on('some-event', f1); 
 emitter.on('some-event', f2); 
 
 emitter.emit('some-event'); 
 
 emitter.removeListener('some-event', f1); 
 
 emitter.emit('some-event'); 

После срабатывания первого события, поскольку f1 и f2 - обе функции будут выполнены. После этого мы удалили f1 из EventEmitter . Когда мы снова испустим событие, выполнится f2

 f1 Triggered 
 f2 Triggered 
 f2 Triggered 

Псевдоним для removeListener() - off() . Например, мы могли бы написать:

 emitter.removeListener('some-event', f1); 

В виде:

 emitter.off('some-event', f1); 

Оба они имеют одинаковый эффект.

removeAllListeners ()

Опять же, как следует из названия, эта функция удалит всех слушателей из всех событий EventEmitter :

 const { EventEmitter } = require('events'); 
 
 const emitter = new EventEmitter(); 
 
 const f1 = () => { 
 console.log('f1 Triggered'); 
 } 
 
 const f2 = () => { 
 console.log('f2 Triggered'); 
 } 
 
 emitter.on('some-event', f1); 
 emitter.on('some-event', f2); 
 
 emitter.emit('some-event'); 
 
 emitter.removeAllListeners(); 
 
 emitter.emit('some-event'); 

Первый emit() запустит и f1 и f2 поскольку они активны в это время. После их удаления emit() выдаст событие, но слушатели не ответят на него:

 f1 Triggered 
 f2 Triggered 

Обработка ошибок

Если вы хотите выдать ошибку с помощью EventEmitter , это должно быть сделано с помощью имени события error Это стандартно для всех EventEmitter в Node.js. Это событие должно также сопровождаться Error объекта. Например, событие ошибки может генерироваться следующим образом:

 myEventEmitter.emit('error', new Error('Something bad happened')); 

Любые прослушиватели error должны иметь обратный вызов с одним аргументом для захвата Error и его корректной обработки. Если EventEmitter генерирует error , но нет подписчиков на error , программа Node.js выдаст Error которая была выдана.

Это в конечном итоге остановит процесс Node.js от запуска и выхода из вашей программы, одновременно отображая трассировку стека для ошибки в консоли.

Предположим, что в нашем классе CountDown countdownTime не может начинаться со значения меньше 2, потому что в противном случае end-soon

В таком случае выдадим сообщение error :

 class CountDown extends EventEmitter { 
 constructor(countdownTime) { 
 super(); 
 
 if (countdownTimer < 2) { 
 this.emit('error', new Error('Value of the countdownTimer cannot be less than 2')); 
 } 
 
 this.countdownTime = countdownTime; 
 this.currentTime = 0; 
 } 
 
 // ........... 
 } 

Обработка этой ошибки обрабатывается так же, как и другие события:

 myCountDown.on('error', (err) => { 
 console.error('There was an error:', err); 
 }); 

Считается хорошей практикой всегда иметь прослушиватель событий error

Собственные модули, использующие EventEmitter

Многие собственные модули в Node.js расширяют EventEmitter и, таким образом, сами являются источниками событий.

Отличный пример - класс Stream . Официальная документация гласит:

Потоки могут быть доступны для чтения, записи или и того, и другого. Все потоки являются экземплярами EventEmitter .

Давайте посмотрим на классическое использование Stream

 const fs = require('fs'); 
 const writer = fs.createWriteStream('example.txt'); 
 
 for (let i = 0; i < 100; i++) { 
 writer.write(`hello, #${i}!\n`); 
 } 
 
 writer.on('finish', () => { 
 console.log('All writes are now complete.'); 
 }); 
 
 writer.end('This is the end\n'); 

Однако между операцией записи и writer.end() мы добавили слушателя. По завершении Stream генерирует finished Другие события, такие как error , pipe и unpipe , когда возникает ошибка или поток чтения передается по конвейеру или не передается из потока записи.

Еще один примечательный класс - это child_process и его метод spawn()

 const { spawn } = require('child_process'); 
 const ls = spawn('ls', ['-lh', '/usr']); 
 
 ls.stdout.on('data', (data) => { 
 console.log(`stdout: ${data}`); 
 }); 
 
 ls.stderr.on('data', (data) => { 
 console.error(`stderr: ${data}`); 
 }); 
 
 ls.on('close', (code) => { 
 console.log(`child process exited with code ${code}`); 
 }); 

Когда child_process записывает в стандартную выходной трубу, data событие stdout (который также extends EventEmitter ) будет срабатывать. Когда выходной поток обнаруживает ошибку, data отправляется из канала stderr .

Наконец, после выхода из процесса запускается событие close

Заключение

Архитектура, управляемая событиями, позволяет нам создавать системы, которые не связаны друг с другом, но очень взаимосвязаны . События представляют собой результат определенного действия, и 1..n слушателей, которые будут прослушивать их и реагировать на них.

В этой статье мы EventEmitter класс EventEmitter и его функции. Мы создали его и использовали напрямую, а также расширили его поведение до настраиваемого объекта.

Наконец, мы рассмотрели некоторые примечательные функции класса.

Как всегда, исходный код доступен на GitHub .

comments powered by Disqus