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

10. Уроки Node.js. Node.JS как веб-сервер

Всем привет! На этом занятии мы познакомимся с Node.js уже в роли веб-сервера. Создадим для этого новое приложение. Настройте пожалуйста свой редактор таким образом чтобы он знал что вы делаете именно Node.js-проект и поддерживал соответствующие автодополнения и глобальные переменные. Далее создадим server.js и в нем первым делом подключаю модуль:

var http = require(‘http’);