Следующий объект который нас интересует, это «EventEmitter» или как его иногда любовно называют, «ee». «EventEmitter» представляет собой основной объект реализующий работу с событиями в Node.JS. Большое количество других встроенных объектов, которые генерируют события в Node.JS, ему наследуют. Для того, чтобы воспользоваться «EventEmitter», достаточно подключить ‘events’, встроенный и взять у него соответствующее свойство.
1 |
var EventEmitter = require('events').EventEmitter; |
после чего я могу создать новый объект
1 |
var server = new EventEmitter; |
и у него есть методы, для работы с событиями. Первый метод это подписка, «on».
1 2 3 |
server.on('request', function(request){ request.approved = true; }); |
Который принимает два аргумента, это имя события — ‘request’ и функцию — обработчик. Я могу указать много подписчиков и все они будут вызваны в том же порядке в котором назначены.
Второй, основной метод это «emit». Он принимает два аргумента, первый это событие которое он должен генерировать, второй это данные которые должен передать.
1 |
server.emit('request', {from: "Клиент"}); |
Эти данные попадают в функцию обработчик. Соответственно, если предположить, что мы пишем веб сервер, то в одном месте кода будет обработчик запросов
1 2 3 |
server.on('request', function(request){ request.approved = true; }); |
Веб сервер — «server», при запросе — ‘request’, что то с ним делает — «function(request)».
А совсем в другом месте кода, например в обработчике входящих соединений, будет «server.emit()», который генерирует события.
Вот наш код целиком
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Демо простейшего применения EE // аргументы передаются по цепочке // обработчики срабатывают в том же порядке, в котором назначены var EventEmitter = require('events').EventEmitter; var server = new EventEmitter; server.on('request', function(request){ request.approved = true; }); server.on('request', function(request){ console.log(request); }); server.emit('request', {from: "Клиент"}); server.emit('request', {from: "Еще Клиент"}); |
Давайте его запустим
Как видите, оба события были обработаны, сначала этим обработчиком
8 9 10 11 12 |
server.on('request', function(request){ request.approved = true; }); |
а потом вот этим
12 13 14 15 16 |
server.on('request', function(request){ console.log(request); }); |
Подробное описание различных методов для работы с событиями, вы конечно найдете в документации, а мы с вами остановимся на том, что принципиально отличает работу с событиями в Node.JS от работы с браузерными событиями. И первое отличие мы сразу можем увидеть из разбора нашего примера. Если браузерные обработчики срабатывают в произвольном порядке, то обработчики событий в Node.JS срабатывают точно в том порядке в котором они были назначены. То есть если у меня есть какие то обработчики, то назначая следующий, я точно уверен, он сработает после предыдущих. Еще одно отличие в том, что в браузере я никак не могу получит список обработчиков, которые назначены на определенный элемент, а в Node.JS это сделать легко.
«emitter.listeners(eventName)» возвращает все обработчики на данное событие, а «emitter.listenerCount(eventName)» возвращает их количество, раньше был «EventEmitter.listenerCount(emitter, eventName)», но как мы видим в доке
Следующее, пожалуй наиболее важное отличие состоит в том, что в «EventEmitter» специальным образом обрабатываются события с названием ‘error’. Если где либо происходит emit этого события и у него нет обработчика, то «EventEmitter» генерирует исключения. таким образом с виду такой безобидный emit
1 2 3 4 5 |
var EventEmitter = require('events').EventEmitter; var server = new EventEmitter; server.emit('error'); // throw TypeError |
повалит весь процесс. Исключения генерируются встроенного типа — «TypeError» — это если в таком виде как на примере выше, а если так
1 2 3 4 5 |
var EventEmitter = require('events').EventEmitter; var server = new EventEmitter; server.emit('error', new Error()); // throw err |
Если есть какой то объект в аргументах, который является «instanceof.Error», например «new Error()» то этот объект будет использован в качестве аргумента «throw» — «thow err».
Например если я запущу этот код
1 2 3 4 5 6 7 8 9 |
// Демо простейшего применения EE // аргументы передаются по цепочке // обработчики срабатывают в том же порядке, в котором назначены var EventEmitter = require('events').EventEmitter; var server = new EventEmitter; server.emit('error'); |
и мы все видим
нода упала с исключением, если же есть обработчик, хоть какой нибудь
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Демо простейшего применения EE // аргументы передаются по цепочке // обработчики срабатывают в том же порядке, в котором назначены var EventEmitter = require('events').EventEmitter; var server = new EventEmitter; server.on('error', function(){ }); server.emit('error'); |
то все будет нормально, запускаю
работает.
Если в «emit» передать объект, который будет описывать, что именно за ошибка была, то он будет передан в обработчик, там мы можем его разобрать и произвести какие либо действия, чтобы обработать.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Демо простейшего применения EE // аргументы передаются по цепочке // обработчики срабатывают в том же порядке, в котором назначены var EventEmitter = require('events').EventEmitter; var server = new EventEmitter; server.on('error', function(err){ console.log(err); }); server.emit('error', new Error("серверная ошибка")); |
Наконец последняя особенность «EventEmitter» о которой сейчас пойдет речь это встроенное средство для борьбы с утечками памяти. Для разбора посмотрим на вот этот пример
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function Request(){ var self = this; this.bigData = new Array(1e6).join('*'); this.send = function(data){ console.log(data); }; this.onError = function(){ self.send("извините, у нас проблема"); }; } setInterval(function(){ var request = new Request(); console.log(process.memoryUsage().heapUsed); }, 200); |
Здесь каждые 200 миллисекунд создается новый объект — «new Request()», и выводится текущее поедание памяти — «console.log(process.memoryUsage().heapUsed);». Объект типа «Request()», в реальной жизни это может быть запрос от клиента, ну а здесь это просто некий объект у которого есть поле — «this.bigData», в котором содержится, что то жирное — «new Array(1e6).join(‘*’)», просто, чтоб было видно сколько памяти на самом деле съедается. Соответственно, если много таких объектов будет в памяти, то есть они тоже будут много. Ну еще у этого объекта есть пара методов,
5 6 7 8 9 10 11 12 13 |
this.send = function(data){ console.log(data); }; this.onError = function(){ self.send("извините, у нас проблема"); }; |
пользоваться мы ими пока не будем, а просто посмотрим что происходит с памятью когда создается много таких объектов. Итак запускаем
Как легко увидеть, память вырастает и очищается, потом опять вырастает и опять очищается. Пока у нас все хорошо и это нормальный режим функционирования Node.JS. Ведь в данном случае «var request = new Request();» это локальная переменная данной функции и после окончания работы функции она нигде не сохраняется, этот объект более не нужен и память из под него можно очистить.
Теперь же немножко расширим этот пример. Добавим объект источника данных, который мы назовем «db» и который может посылать какую то информацию, которую «Request()» может в свою очередь пересылать клиенту.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
var EventEmitter = require('events').EventEmitter; var db = new EventEmitter(); function Request(){ var self = this; this.bigData = new Array(1e6).join('*'); this.send = function(data){ console.log(data); }; db.on('data', function(info){ self.send(info); }); } setInterval(function(){ var request = new Request(); console.log(process.memoryUsage().heapUsed); }, 200); |
изменение не большое, посмотрим к чему это приведет при запуске кода
ого, какой то 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)» и запустить код еще раз
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var EventEmitter = require('events').EventEmitter; var db = new EventEmitter(); function Request(){ var self = this; this.bigData = new Array(1e6).join('*'); this.send = function(data){ console.log(data); }; db.on('data', function(info){ self.send(info); }); } setInterval(function(){ var request = new Request(); console.log(process.memoryUsage().heapUsed); console.log(db); }, 200); |
Здесь мы видим объект «db» и вот как раз свойство «events» в котором находятся обработчики, и оно действительно все время увеличивается по размеру. Было сначала маленькое, потом растет функций все больше и каждая функция, через замыкание, тянет за собой весь объект «Request()». А вот и warning
Оказывается у «EventEmitter» есть максимальное число обработчиков, которых можно назначить и оно равно 10. Как только это число превышается, то он вот такое предупреждение выводит, что может быть утечка памяти. Которая в нашем случае как раз и произошла. Что делать? Как вариант, можно, например, взять и после окончания обработки запроса убрать обработчики на событие ‘data’. Для этого код нужно немного переписать, добавить вызов метода «request.end()» в конце.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
var EventEmitter = require('events').EventEmitter; var db = new EventEmitter(); function Request(){ var self = this; this.bigData = new Array(1e6).join('*'); this.send = function(data){ console.log(data); }; function onData(info){ self.send(info); } this.end = function(){ db.removeListener('data', onData) }; db.on('data', onData); } setInterval(function(){ var request = new Request(); request.end(); console.log(process.memoryUsage().heapUsed); console.log(db); }, 200); |
И вот, что будет при таком вызове.
Теперь все хорошо, никакой утечки памяти не происходит.
Когда такой сценарий наиболее опасен? Он наиболее опасен в тех случаях, если по какой то причине максимальное количество обработчиков отключают, то есть делают — «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, они нам еще пригодятся.