13Sep

16610_a6a5_3
Следующий объект, который нас интересует, это 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, они нам еще пригодятся.

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

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

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

21. Уроки Node.js. Writable Поток Ответа res, Метод pipe. Pt.2

По окончании файла наступит событие end, в обработчике которого мы завершим ответ вызовом res.end. Таким образом, будет закрыто исходящее соединение, потому что файл полностью отослан. Получившийся код является весьма универсальным:

15. Уроки Node.js. Асинхронная разработка. Введение.

В реальной жизни очень редко бывает так, что, получив запрос, сервер может тут же на него ответить. Обычно для того, чтобы ответить, серверу нужны какие-то данные. Эти данные он получает либо из базы, либо из какого-то другого источника, например, из файловой системы. В этом примере, используя модуль fs при получении запроса на url ‘/’, считывается файл index.html и выводится посетителю.