9. Уроки Node.js. События, EventEmitter и Утечки Памяти

16610_a6a5_3
Следующий объект, который нас интересует, это EventEmitter или, как его иногда называют, ЕЕ.  EventEmitter представляет собой основной объект, реализующий работу с событиями в Node.js. Большое количество других встроенных объектов, которые генерируют события, ему наследуют. Для того чтобы воспользоваться EventEmitter достаточно подключить модуль “events”встроенный и взять с него соответствующее свойство (создадим ee.js) :

После чего я могу создать новый объект:

У него есть методы для работы с событиями. Первый метод – это подписка:

оn – имя события, function – обработчик. Я могу указать много подписчиков, и все они будут вызваны в том же порядке, в котором назначены.

Второй основной метод – это emit:

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

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

Давайте запустим этот код:

Как видите, оба события были обработаны. Сначала первым обработчиком, а потом вторым.

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

Если браузерные обработчики срабатывают в произвольном порядке, то Node обработчики точно в том порядке,  в котором были назначены. То есть, если у меня есть какие-то обработчики, то назначая следующие, я точно уверен, он сработает после предыдущих.

Еще одно отличие в том, что в браузере я никак не могу получить список обработчиков, в которых назначен определенный элемент. А в Node.js это сделать легко: emitter.listeners(eventName) возвращает все обработчики на данное событие. А emitter.listenerCount(eventName) позволяет получить их общее количество.

Следующее, наиболее важное, отличие состоит в том, что в EventEmitter специальным образом обрабатывается событие с названием error:

Если где-либо происходит emit этого события, и у него нет обработчика, то EventEmitter генерирует исключения. Таким образом, с виду такой безобидный  emit “повалит весь процесс. Исключения генерируются встроенного типа // throw TypeError, если в вот таком виде:

А если есть какой-нибудь объект в аргументах, например:

То этот объект будет использован в качестве аргумента // throw err. Вот так это работает. Например, запущу этот файл (с первым вариантом server.emit), и мы все видим , что Node упала с исключением. Если есть хоть какой-нибудь обработчик, то все будет нормально:

Если в emit передать объект, который будет описать что именно за ошибка была, то он будет передан в обработчик, там можем его разобрать и произвести какие-то действия, чтобы обработать.

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

Здесь каждые 200 миллисекунд создается новый объект типа request и выводится текущее поедание памяти. Объект типа request, в реальной жизни это может быть запрос от клиента, ну а здесь это просто некий объект, у которого есть поле bigData, в котором содержится что-то жирное, чтобы было видно, сколько памяти, на самом деле, съедается. Соответственно, если много таких объектов будет в памяти, то есть они будут тоже очень много. Ну, и еще у этого объекта есть пара методов. Пользоваться ими мы не будем, а просто посмотрим, что происходит с памятью, когда создается много таких объектов.

Итак, запускаем node leak.js. Легко увидеть, что память вырастает, а потом очищается. Затем опять вырастает и очищается. Пока у нас все хорошо. Это нормальный режим функционирования Node.js. Ведь в данном случае requestэто локальная переменная данной функции:

После окончания работы функции она нигде не сохраняется. Этот объект больше не нужен, и память из-под него можно очистить.

Теперь немного расширим этот пример. Добавим объект источника данных, который назовем db.

Он  может посылать какую-то информацию, которую request может, в свою очередь, присылать клиенту:

Изменение небольшое. Посмотрим, к чему это приведет при запуске кода. Мы видим какой-то Warning. Память постоянно растет. В чем же дело? Для того, чтобы это понять немного глубже познакомимся с логарифмом работы EventEmitter, а именно с тем, как работают эти события, что происходит, когда вызывают db.on data. Информацию о том, что я поставил обработчик, нужно где-то запомнить. Действительно, она запоминается в специальном свойстве объекта db. В этом свойстве находятся все обработчики события, которые назначены. Когда происходит вызов emit, они из него берутся и вызываются. Теперь уже можно понять, отчего возникла утечка. Несмотря на то, что request здесь больше не нужен, эта функция находится в свойствах объекта db. И получается так, что каждый request, который создается, сохраняет там внутри эту функцию. А эта функция ссылается через  замыкание на вообще весь объект, и получается, что этот обработчик привязывает request к db. Пока живет db, будет жить и request. Если db живет  очень долго, то и request тоже будет жить очень долго. Происходящее можно легко увидеть, если добавить и запустить код еще раз.

console.log(db);

Стоп! Мы увидели достаточно. Вот объект db, и есть свойство events, в котором находятся обработчики:

И оно, действительно, все время увеличивается по размеру. Сначала было маленькое, потом функций все больше и больше. И каждая функция через замыкание тянет за собой весь объект request.
Есть еще и warning в нашей console. Оказывается, в EventEmitter есть по умолчанию максимальное число обработчиков, которые можно назначить. Оно равно  10. Как только это число превышается, то он выводит предупреждение о том, что может быть утечка памяти, которая, в нашем случае, как раз и произошла. Что делать? Как вариант, можно, например, после окончания обработки запроса убрать обработчики на событие  data. Для этого нужно код немножко переписать, добавить вызов метода end в конце, и при таком вызове будет все хорошо.

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

db.setMaxListeners(0);

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

Как отследить эти утечки? Это достаточно проблематично. Может помочь модуль heapdump, который позволяет делать снимок памяти Node.js и потом анализировать его в Chrome. Но лучшая защита – это думать, что делаешь, когда привязываешь короткоживущие объекты в случай события долгоживущих.  А также помнить о том, что может понадобиться от них отвязаться, чтобы память была очищена.

Итак, EventEmitterэто один из самых важных и широко используемых объектов в Node.js. Сам по себе он используется редко. В основном используются наследники этого класса, такие как объект запроса, объект сервера и много всего другого. Мы с этим столкнемся в ближайшем будущем. Для генерации события используется вызов emit:

emit(event,args…)->on(event, args…)

Ему передаются названия событий и какие-то аргументы, данные. При этом, он вызывает обработчики, назначенные через on.

EventEmitter гарантирует, что обработчики будут вызваны в том же порядке. При этом, в отличие от браузерных обработчиков, всегда можно проверить, есть ли какие-то обработчики на определенное событие. Кроме того, сам метод emit, если событие было обработано, возвращает true а иначе false. Используется это достаточно редко.

В EventEmitter есть специальное событие, которое называется error:

emit(error) без обработчиков -> throw

Если на это событие нет обработчиков, то это приводит к тому, что EventEmitter сам делает throw. Казалось бы, зачем? Но, как мы скоро убедимся, это решение очень мудрое и полезное. Потому что многие встроенные объекты Node.js сообщают о своих ошибках именно так, через emit(error).
И без такого throw их было бы очень легко пропустить, забыть о них и потом долго искать что же и где случилось.

И, наконец, последнее, в EventEmitter есть встроенные средства по борьбе с утечкой памяти. Сейчас они нам не очень нужны, но в дальнейшем, когда мы будем делать проект на Node.js, они нам еще пригодятся.

Вы можете скачать код данного урока в репозитории.

n1JRsFeBМатериалы урока взяты из следующего скринкаста.

We are looking forward to meeting you on our website soshace.com

About the author

Stay Informed

It's important to keep up
with industry - subscribe!

Stay Informed

Looks good!
Please enter the correct name.
Please enter the correct email.
Looks good!

Related articles

Уроки Express.js . Логгер, Конфигурация, Шаблонизация с EJS. Часть 2.

Favicon – это все connect Middleware, он смотрит, если url имеет вид favicon.ico, то он читает favicon и ...

3. Уроки Express.js. Шаблонизация с EJS: Layout, Block, Partials

В реальной жизни у нас обычно больше, чем один шаблон. Более того, если уж так ...

24.11.2016

Уроки Express.js. Основы и Middleware. Часть 2.

Всем привет! Давайте продолжим наш урок об основах Express и Middleware. Итог (добавим в ...

Sign in

Forgot password?

Or use a social network account

 

By Signing In \ Signing Up, you agree to our privacy policy

Password recovery

You can also try to

Or use a social network account

 

By Signing In \ Signing Up, you agree to our privacy policy