Следующий объект, который нас интересует, это EventEmitter или, как его иногда называют, ЕЕ. EventEmitter представляет собой основной объект, реализующий работу с событиями в Node.js. Большое количество других встроенных объектов, которые генерируют события, ему наследуют. Для того чтобы воспользоваться EventEmitter достаточно подключить модуль “events”встроенный и взять с него соответствующее свойство (создадим ee.js) :
var EventEmitter = require('events').EventEmitter;
После чего я могу создать новый объект:
var server = new EventEmitter;
У него есть методы для работы с событиями. Первый метод – это подписка:
server.on('request', function(request) { request.approved = true; });
оn – имя события, function – обработчик. Я могу указать много подписчиков, и все они будут вызваны в том же порядке, в котором назначены.
Второй основной метод – это emit:
server.emit('request', {from: "Client"}); server.emit('request', {from: "Another Client"});
Он генерирует события и передает данные. Эти данные попадают в функцию обработчика. Соответственно, если предположить, что мы пишем веб сервер, то в одном месте кода будут обработчики запроса. Веб сервер при запросе что-то с ним делает:
server.on('request', function(request) { request.approved = true; });
А затем, в другом месте кода, например, в обработчике входящих соединений, будет сервер emit, который генерирует события.
Давайте запустим этот код:
// Demo of the simpliest usage of EE // arguments are passed along the chain // handlers are triggered in the same order in which the designated 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: "Client"}); server.emit('request', {from: "Another Client"});
Как видите, оба события были обработаны. Сначала первым обработчиком, а потом вторым.
Подробное описание различных методов для работы с событиями вы, конечно, найдете в документации, а мы с вами остановимся на том, что принципиально отличает работу с событиями в Node.js по сравнению с работой с событиями в браузерах. Первое отличие мы сразу же сможем увидеть из разбора этого примера:
server.on('request', function(request) { request.approved = true; }); server.on('request', function(request) { console.log(request); }); server.emit('request', {from: "Client"}); server.emit('request', {from: "Another Client"});
Если браузерные обработчики срабатывают в произвольном порядке, то Node обработчики точно в том порядке, в котором были назначены. То есть, если у меня есть какие-то обработчики, то назначая следующие, я точно уверен, он сработает после предыдущих.
Еще одно отличие в том, что в браузере я никак не могу получить список обработчиков, в которых назначен определенный элемент. А в Node.js это сделать легко: emitter.listeners(eventName) возвращает все обработчики на данное событие. А emitter.listenerCount(eventName) позволяет получить их общее количество.
Следующее, наиболее важное, отличие состоит в том, что в EventEmitter специальным образом обрабатывается событие с названием error:
// Demo of the simpliest usage of EE // arguments are passed along the chain // handlers are triggered in the same order in which the designated var EventEmitter = require('events').EventEmitter; var server = new EventEmitter; server.on('error', function(err) {
Если где-либо происходит emit этого события, и у него нет обработчика, то EventEmitter генерирует исключения. Таким образом, с виду такой безобидный emit “повалит“ весь процесс. Исключения генерируются встроенного типа // throw TypeError, если в вот таком виде:
server.emit('error')
А если есть какой-нибудь объект в аргументах, например:
server.emit('error' new Error());
То этот объект будет использован в качестве аргумента // throw err. Вот так это работает. Например, запущу этот файл (с первым вариантом server.emit), и мы все видим , что Node упала с исключением. Если есть хоть какой-нибудь обработчик, то все будет нормально:
// Demo of the simpliest usage of EE // arguments are passed along the chain // handlers are triggered in the same order in which the designated var EventEmitter = require('events').EventEmitter; var server = new EventEmitter; server.on('error', function() { }); server.emit('error');
Если в emit передать объект, который будет описать что именно за ошибка была, то он будет передан в обработчик, там можем его разобрать и произвести какие-то действия, чтобы обработать.
server.on('error', function(err) { //.. }); server.emit('error', new Error ("server error"));
Последняя особенность EventEmitter, о которой сейчас пойдет речь, это встроенное средство для борьбы с утечками памяти. Для разбора у нас есть пример:
function Request() { var self = this; this.bigData = new Array(1e6).join('*'); this.send = function(data) { console.log(data); }; this.onError = function(data) { self.send("извините, у нас проблема"); }; } setInterval(function() { var request = new Request(); console.log(process.memoryUsage().heapUsed); }, 200);
Здесь каждые 200 миллисекунд создается новый объект типа request и выводится текущее поедание памяти. Объект типа request, в реальной жизни это может быть запрос от клиента, ну а здесь это просто некий объект, у которого есть поле bigData, в котором содержится что-то жирное, чтобы было видно, сколько памяти, на самом деле, съедается. Соответственно, если много таких объектов будет в памяти, то есть они будут тоже очень много. Ну, и еще у этого объекта есть пара методов. Пользоваться ими мы не будем, а просто посмотрим, что происходит с памятью, когда создается много таких объектов.
Итак, запускаем node leak.js. Легко увидеть, что память вырастает, а потом очищается. Затем опять вырастает и очищается. Пока у нас все хорошо. Это нормальный режим функционирования Node.js. Ведь в данном случае request – это локальная переменная данной функции:
var request = new Request();
После окончания работы функции она нигде не сохраняется. Этот объект больше не нужен, и память из-под него можно очистить.
Теперь немного расширим этот пример. Добавим объект источника данных, который назовем db.
var EventEmitter = require('events').EventEmitter; var db = new EventEmitter();
Он может посылать какую-то информацию, которую request может, в свою очередь, присылать клиенту:
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); }); }
Изменение небольшое. Посмотрим, к чему это приведет при запуске кода. Мы видим какой-то Warning. Память постоянно растет. В чем же дело? Для того, чтобы это понять немного глубже познакомимся с логарифмом работы EventEmitter, а именно с тем, как работают эти события, что происходит, когда вызывают db.on data. Информацию о том, что я поставил обработчик, нужно где-то запомнить. Действительно, она запоминается в специальном свойстве объекта db. В этом свойстве находятся все обработчики события, которые назначены. Когда происходит вызов emit, они из него берутся и вызываются. Теперь уже можно понять, отчего возникла утечка. Несмотря на то, что request здесь больше не нужен, эта функция находится в свойствах объекта db. И получается так, что каждый request, который создается, сохраняет там внутри эту функцию. А эта функция ссылается через замыкание на вообще весь объект, и получается, что этот обработчик привязывает request к db. Пока живет db, будет жить и request. Если db живет очень долго, то и request тоже будет жить очень долго. Происходящее можно легко увидеть, если добавить и запустить код еще раз.
console.log(db);
Стоп! Мы увидели достаточно. Вот объект db, и есть свойство events, в котором находятся обработчики:
domain: null, _events: { data: [ [Function], [Function], [Function], [Function], [Function], [Function], [Function], [Function] ] }, _eventsCount: 1, _maxListeners: undefined }
И оно, действительно, все время увеличивается по размеру. Сначала было маленькое, потом функций все больше и больше. И каждая функция через замыкание тянет за собой весь объект request.
Есть еще и warning в нашей console. Оказывается, в EventEmitter есть по умолчанию максимальное число обработчиков, которые можно назначить. Оно равно 10. Как только это число превышается, то он выводит предупреждение о том, что может быть утечка памяти, которая, в нашем случае, как раз и произошла. Что делать? Как вариант, можно, например, после окончания обработки запроса убрать обработчики на событие data. Для этого нужно код немножко переписать, добавить вызов метода end в конце, и при таком вызове будет все хорошо.
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.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, они нам еще пригодятся.
Вы можете скачать код данного урока в репозитории.
Материалы урока взяты из следующего скринкаста.
We are looking forward to meeting you on our website soshace.com