13. События, EventEmitter и утечки памяти

Следующий объект который нас интересует, это «EventEmitter» или как его иногда любовно называют, «ee». «EventEmitter» представляет собой основной объект реализующий работу с событиями в Node.JS. Большое количество других встроенных объектов, которые генерируют события в Node.JS, ему наследуют. Для того, чтобы воспользоваться «EventEmitter», достаточно подключить ‘events’, встроенный и взять у него соответствующее свойство.

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

и у него есть методы, для работы с событиями. Первый метод это подписка, «on».

Который принимает два аргумента, это имя события — ‘request’ и функцию — обработчик. Я могу указать много подписчиков и все они будут вызваны в том же порядке в котором назначены.

Второй, основной метод это «emit». Он принимает два аргумента, первый это событие которое он должен генерировать, второй это данные которые должен передать.

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

Веб сервер — «server», при запросе — ‘request’,  что то с ним делает — «function(request)».

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

Вот наш код целиком

Давайте его запустим

Screenshot_13_01

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

а потом вот этим

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

Screenshot_13_02

«emitter.listeners(eventName)» возвращает все обработчики на данное событие, а «emitter.listenerCount(eventName)» возвращает их количество, раньше был «EventEmitter.listenerCount(emitter, eventName)», но как мы видим в доке

Screenshot_13_03

Следующее, пожалуй наиболее важное отличие состоит в том, что в «EventEmitter» специальным образом обрабатываются события с названием ‘error’. Если где либо происходит emit этого события и у него нет обработчика, то «EventEmitter» генерирует исключения. таким образом с виду такой безобидный emit

повалит весь процесс. Исключения генерируются встроенного типа — «TypeError» — это если в таком виде как на примере выше, а если так

Если есть какой то объект в аргументах, который является «instanceof.Error», например «new Error()» то этот объект будет использован в качестве аргумента «throw» — «thow err».

Например если я запущу этот код

и мы все видим

screenshot_13_04

нода упала с исключением, если же есть обработчик, хоть какой нибудь

то все будет нормально, запускаю

screenshot_13_05

работает.

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

screenshot_13_06

Наконец последняя особенность «EventEmitter» о которой сейчас пойдет речь это встроенное средство для борьбы с утечками памяти. Для разбора посмотрим на вот этот пример

Здесь каждые 200 миллисекунд создается новый объект — «new Request()», и выводится текущее поедание памяти — «console.log(process.memoryUsage().heapUsed);». Объект типа «Request()», в реальной жизни это может быть запрос от клиента, ну а здесь это просто некий объект у которого есть поле — «this.bigData», в котором содержится, что то жирное — «new Array(1e6).join(‘*’)», просто, чтоб было видно сколько памяти на самом деле съедается. Соответственно, если много таких объектов будет в памяти, то есть они тоже будут много. Ну еще у этого объекта есть пара методов,

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

screenshot_13_07

Как легко увидеть, память вырастает и очищается, потом опять вырастает и опять очищается.  Пока у нас все хорошо и это нормальный режим функционирования Node.JS. Ведь в данном случае «var request = new Request();» это локальная переменная данной функции и после окончания работы функции она нигде не сохраняется, этот объект более не нужен и память из под него можно очистить.

Теперь же немножко расширим этот пример. Добавим объект источника данных, который мы назовем «db» и который может посылать какую то информацию, которую «Request()» может в свою очередь пересылать клиенту.

изменение не большое, посмотрим к чему это приведет при запуске кода

screenshot_13_08

ого, какой то warning, память постоянно растет, в чем же дело? Для того, чтобы это понять, немножко глубже познакомимся  с алгоритмом работы «EventEmitter», а именно с тем как работают вообще эти события, что происходит когда я вызываю «db.on()».  Информацию о том, что я поставил где то обработчик, нужно где то запомнить и действительно она запоминается в специальном свойстве объекта «db».  В этом свойстве находятся все обработчики событий, которые назначены и когда происходит вызов «db.emit()», то они из него берут и вызываются. Теперь можно понять, от чего возникла утечка. Ведь не смотря на то, что «var request = new Request();» здесь по идеи больше не нужен, как в прошлом примере, эта функция — обработчик, которую мы передаем «db.on()» в качестве аргумента — «function(info){ self.send(info); }» находится в свойствах объекта «db». И получается так, что каждый «Request()» который создается сохраняет там внутри объекта «db» функцию обработчик «function(info){ self.send(info); }», а она, через замыкание, ссылается на весь объект «Request()» и получается что обработчик «db.on()» привязывает «Request()» к «db». Пока живет «db» будет жить и «Request()». Происходящие можно легко увидеть если добавить «consol.log(db)» и запустить код еще раз

screenshot_13_09

Здесь мы видим объект «db» и вот как раз свойство «events» в котором находятся обработчики, и оно действительно все время увеличивается по размеру. Было сначала маленькое, потом растет функций все больше и каждая функция, через замыкание, тянет за собой весь объект «Request()». А вот и warning

screenshot_13_10

Оказывается у «EventEmitter» есть максимальное число обработчиков, которых можно назначить и оно равно 10. Как только это число превышается, то он вот такое предупреждение выводит, что может быть утечка памяти. Которая в нашем случае как раз и произошла. Что делать? Как вариант, можно, например, взять и после окончания обработки запроса убрать обработчики на событие ‘data’. Для этого код нужно немного переписать, добавить вызов метода «request.end()» в конце.

И вот, что будет при таком вызове.

screenshot_13_11

Теперь все хорошо, никакой утечки памяти не происходит.

Когда такой сценарий наиболее опасен? Он наиболее опасен в тех случаях, если по какой то причине максимальное количество обработчиков отключают, то есть делают — «db.setMaxListners(0);», предполагая, что много кто может подписываться на эти события. Действительно бывают такие источники событий для которых много много подписчиков возможно и нужно отменить этот лимит. Соответственно лимит отменяют, а вот убирать обработчики забывают. И вот это как раз и приводит к тому, что нода растет и растет  в памяти.

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

Итак, «require(‘events’).EventEmitter» один из самых важных и широко используемых объектов в Node.JS. Сам по себе «EventEmitter», на самом деле используется редко, в основном используются наследники этого класса, такие как объект запроса, объект сервера и много, много всего другого, мы с этим в ближайшем будущем уже столкнемся.

  • Для генерации события используется «emit» -> «emit(event, args…) -> on(event, args…)»  ему передается название события и какие то аргументы данные, при этом он вызывает обработчики, назначенные через «on()».
  • Сохраняет порядок обработчиков. Гарантирует, что обработчики будут вызваны в том же порядке.
  • Можно проверить наличие обработчиков. При этом в отличии от браузерных обработчиков всегда можно проверить, есть ли какие то обработчики на определенное событие.
    Получить обработчики — «emitter.listeners(event)»
    Получить количество обработчиков — «EventEmitter.listenerCount(emitter, event)»
    Кроме того сам метод «emit()», если событие было обработано, возвращает «true» если нет, то «false». Правда используется эта фишка достаточно редко.
  • «emit(error) без обработчиков -> throw. Дальше в «EventEmitter» есть специальное событие, называется «error», если на это событие нет обработчиков, то это приводит к тому, что «EventEmitter» сам делает «thow». Казалось бы зачем, но как мы скоро убедимся, это решение очень мудрое и полезное, потому что многие встроенные объекты в Node.JS сообщают о своих ошибках именно так, через «emit(error)» и без такого «throw» их было бы очень легко пропустить, забыть о них и потом долго искать, что же где случилось.
  • Борется с утечками памяти. И наконец последнее, в «EventEmitter» есть встроенные средства, по борьбе с утечками памяти. Сейчас они нам пока не очень нужны, но в дальнейшем. когда мы будем делать проект на Node.JS, они нам еще пригодятся.