1. Code
  2. JavaScript
  3. Node

Usando el módulo de eventos de Node

Cuando escuché por primera vez sobre Node.js, pensé que era sólo una implementación de JavaScript para el servidor. Pero en realidad es mucho más que eso: viene con una gran cantidad de funciones integradas que no se incluyen en el navegador. Una de esas funcionalidades es el módulo de eventos, que tiene la clase EventEmitter. Lo veremos en este tutorial.
Scroll to top

Spanish (Español) translation by Andrea Jiménez (you can also view the original English article)

Cuando escuché por primera vez sobre Node.js, pensé que era sólo una implementación de JavaScript para el servidor. Pero en realidad es mucho más que eso: viene con una gran cantidad de funciones integradas que no se incluyen en el navegador. Una de esas funcionalidades es el módulo de eventos, que tiene la clase EventEmitter. Lo veremos en este tutorial.


EventEmitter: Qué y por qué

Una de las últimas ventajas de los eventos: son una forma muy flexible de unir partes de tu código.

Entonces, ¿qué hace exactamente la clase EventEmitter? En pocas palabras, te permite escuchar "eventos" y asignar acciones para que se ejecuten cuando ocurran esos eventos. Si estás familiarizado con JavaScript de front-end, sabrás sobre los eventos del mouse y el teclado que ocurren en ciertas interacciones del usuario. Estos son muy similares, excepto que podemos emitir eventos por nuestra cuenta, cuando queremos, y no es necesario basándonos en la interacción del usuario. Los principios de la clase EventEmitter se basan en el llamado modelo de publicación/suscripción, porque podemos suscribirnos a eventos y luego publicarlos. Hay muchas bibliotecas de front-end creadas con soporte pub/sub, pero Node las tiene integradas.

La otra pregunta importante es esta: ¿por qué usarías el modelo de eventos? En Node, es una alternativa a las devoluciones de llamada profundamente anidadas. Muchos métodos de Node se ejecutan de forma asincrónica, lo que significa que para ejecutar el código después de que el método haya finalizado, debes pasar un método de devolución de llamada a la función. Eventualmente, tu código se verá como un embudo gigante. Para evitar esto, muchas clases de nodos emiten eventos que puedes escuchar. Esto te permite organizar el código de la forma que quieras y no usar las devoluciones de llamada.

Una de las últimas ventajas de los eventos: son una forma muy flexible de unir partes de tu código. Se puede emitir un evento, pero si ningún código lo está escuchando, está bien: simplemente pasará desapercibido. Esto significa que eliminar los oyentes (o las emisiones de eventos) nunca produce errores de JavaScript.


Usando EventEmitter

Comenzaremos con la clase EventEmitter por sí sola. El acceso es bastante simple: sólo necesitamos el módulo de eventos:

1
2
    var events = require("events");

Este objeto events tiene una sola propiedad, que es la clase EventEmitter en sí. Por lo tanto, vamos a hacer un ejemplo sencillo para los principiantes:

1
2
    var EventEmitter = require("events").EventEmitter;
3
4
    var ee = new EventEmitter();
5
    ee.on("someEvent", function () {
6
        console.log("event has occured");
7
    });
8
9
    ee.emit("someEvent");

Comenzamos creando un nuevo objeto EventEmitter. Este objeto tiene dos métodos principales que usamos para eventos: on y emit.

Comenzamos con on. Este método toma dos parámetros: comenzamos con el nombre del evento que estamos escuchando: en este caso, es "someEvent". Pero por supuesto, podría ser cualquier cosa, y por lo general elegirás algo mejor. El segundo parámetro es la función que se llamará cuando ocurra el evento. Eso es todo lo que se requiere para organizar un evento.

Ahora, para activar el evento, pasa el nombre del evento al método emit de la instancia de EventEmitter. Esa es la última línea del código anterior. Si ejecutas ese código, verás que el texto se imprime en la consola.

Ese es el uso más básico de un EventEmitter. También puedes incluir datos al activar los eventos:

1
2
    ee.emit("new-user", userObj);

Ese es solo un parámetro de datos, pero puedes incluir tantos como quieras. Para usarlos en tu función de controlador de eventos, simplemente tómalos como parámetros:

1
2
    ee.on("new-user", function (data) {
3
        // use data here

4
    });

Antes de continuar, permíteme aclarar parte de la funcionalidad de EventEmitter. Podemos tener más de un oyente para cada evento; se pueden asignar varios oyentes de eventos (todos con on) y todas las funciones se llamarán cuando se active el evento. De forma predeterminada, Node permite hasta diez oyentes en un evento a la vez; si se crean más, el nodo emitirá una advertencia. Sin embargo, podemos cambiar esta cantidad usando la función setMaxListeners. Por ejemplo, si ejecutas esto, deberías ver una advertencia impresa sobre la salida:

1
2
    ee.on("someEvent", function () { console.log("event 1"); });
3
    ee.on("someEvent", function () { console.log("event 2"); });
4
    ee.on("someEvent", function () { console.log("event 3"); });
5
    ee.on("someEvent", function () { console.log("event 4"); });
6
    ee.on("someEvent", function () { console.log("event 5"); });
7
    ee.on("someEvent", function () { console.log("event 6"); });
8
    ee.on("someEvent", function () { console.log("event 7"); });
9
    ee.on("someEvent", function () { console.log("event 8"); });
10
    ee.on("someEvent", function () { console.log("event 9"); });
11
    ee.on("someEvent", function () { console.log("event 10"); });
12
    ee.on("someEvent", function () { console.log("event 11"); });
13
14
    ee.emit("someEvent");

Para establecer el número máximo de espectadores, agrega esta línea sobre los oyentes:

1
2
    ee.setMaxListeners(20);

Ahora, no recibirás una advertencia cuando lo ejecutes.


Otros métodos de EventEmitter

Hay algunos otros métodos de EventEmitter que encontrarás útiles.

Este es uno muy bueno: once. Es como el método on, excepto que sólo funciona una vez. Después de ser llamado por primera vez, se elimina el oyente.

1
2
    ee.once("firstConnection", function () { console.log("You'll never see this again"); });
3
    ee.emit("firstConnection");
4
    ee.emit("firstConnection");

Si ejecutas esto, solo verás el mensaje una vez. La segunda emisión del evento no es captada por ningún oyente (y eso está bien, por cierto), porque el oyente once se eliminó después de usarse una vez.

Hablando de eliminar los oyentes, podemos hacerlo nosotros mismos, manualmente, de varias maneras. En primer lugar, podemos quitar un solo oyente con el método removeListener. Toma dos parámetros: el nombre del evento y la función de escucha. Hasta ahora, hemos estado usando funciones anónimas como nuestros oyentes. Si queremos poder eliminar un oyente más adelante, deberá ser una función con un nombre al que podamos hacer referencia. Podemos usar el método removeListener para duplicar los efectos del método once:

1
2
    function onlyOnce () {
3
        console.log("You'll never see this again");
4
        ee.removeListener("firstConnection", onlyOnce);
5
    }
6
7
    ee.on("firstConnection", onlyOnce) 
8
    ee.emit("firstConnection");
9
    ee.emit("firstConnection");

Si ejecutas esto, verás que tiene el mismo efecto que el método once.

Si quieres eliminar todos los oyentes vinculados a un evento determinado, puedes usar el método removeAllListeners; solo pásale el nombre del evento:

1
2
    ee.removeAllListeners("firstConnection");

Para eliminar todos los oyentes de todos los eventos, llama a la función sin ningún parámetro.

1
ee.removeAllListeners();

Hay un último método: listener. Este método toma un nombre de evento como parámetro y devuelve una matriz de todas las funciones que están escuchando ese evento. Este es un ejemplo de eso, basado en nuestro ejemplo de onlyOnce:

1
2
    function onlyOnce () {
3
        console.log(ee.listeners("firstConnection"));
4
        ee.removeListener("firstConnection", onlyOnce);
5
        console.log(ee.listeners("firstConnection"));
6
    }
7
8
    ee.on("firstConnection", onlyOnce) 
9
    ee.emit("firstConnection");
10
    ee.emit("firstConnection");

Terminaremos esta sección con un poco de meta-ness. Nuestra instancia de EventEmitter en sí, activa dos eventos propios, que podemos escuchar: uno cuando creamos nuevos oyentes, y otro cuando los eliminamos. Mira esto:

1
2
    ee.on("newListener", function (evtName, fn) {
3
        console.log("New Listener: " + evtName);
4
    });
5
6
    ee.on("removeListener", function (evtName) {
7
        console.log("Removed Listener: " + evtName);
8
    });
9
10
    function foo () {}
11
12
    ee.on("save-user", foo);
13
    ee.removeListener("save-user", foo);

Al ejecutarlo, verás que se han ejecutado nuestros oyentes tanto para los nuevos oyentes como para los eliminados, y obtenemos los mensajes que esperábamos.

Entonces, ahora que hemos visto todos los métodos que tiene una instancia de EventEmitter, veamos cómo funciona junto con otros módulos.

Módulos internos de EventEmitter

Dado que la clase EventEmitter es simplemente JavaScript normal, tiene mucho sentido que se pueda usar dentro de otros módulos. Puedes crear instancias de EventEmitter y usarlas para controlar eventos internos dentro de tus propios módulos de JavaScript. Aunque eso es simple. Más interesante, sería crear un módulo que herede de EventEmitter, para que podamos usar su funcionalidad como parte de la API pública.

En realidad, hay módulos de Node integrados que hacen exactamente esto. Por ejemplo, puedes estar familiarizado con el módulo http; este es el módulo que usarás para crear un servidor web. Este ejemplo básico muestra cómo el método on de la clase EventEmitter se ha convertido en parte de la clase http.Server:

1
2
    var http = require("http");
3
    var server = http.createServer();
4
5
    server.on("request", function (req, res) {
6
        res.end("this is the response");
7
    });
8
9
    server.listen(3000);

Si ejecutas este fragmento de código, el proceso esperará una solicitud; puedes ir a http://localhost:3000 y obtendrás la respuesta. Cuando la instancia del servidor recibe la solicitud de tu navegador, emite un evento "request", un evento que nuestro oyente recibirá y podrá actuar sobre él.

Entonces, ¿cómo podemos crear una clase que herede de EventEmitter? En realidad no es tan difícil. Crearemos una clase UserList simple, la cual controla los objetos del usuario. Entonces, en un archivo userlist.js, comenzaremos con esto:

1
2
    var util         = require("util");
3
    var EventEmitter = require("events").EventEmitter;

Necesitamos el módulo util para ayudar con la herencia. Luego, necesitamos una base de datos: en lugar de usar una base de datos real, solo usaremos un objeto:

1
2
    var id = 1;
3
    var database = {
4
        users: [
5
            { id: id++, name: "Joe Smith",  occupation: "developer"    },
6
            { id: id++, name: "Jane Doe",   occupation: "data analyst" },
7
            { id: id++, name: "John Henry", occupation: "designer"     }
8
        ]
9
    };

Ahora, podemos crear nuestro módulo. Si no estás familiarizado con los módulos de Node, así es como funcionan: cualquier JavaScript que escribamos dentro de este archivo solo se puede leer desde el interior del archivo, de forma predeterminada. Si queremos que haga parte de la API pública del módulo, lo convertimos en una propiedad de module.exports o asignamos un objeto o función completamente nuevo a module.exports. Hagámoslo:

1
2
    function UserList () {
3
        EventEmitter.call(this);
4
    }

Esta es la función constructora, pero no es tu función constructora de JavaScript habitual. Lo que estamos haciendo aquí es usar el método call en el constructor de la función EventEmitter para ejecutar ese método en el nuevo objeto UserList (que es this). Si necesitamos hacer cualquier otra inicialización a nuestro objeto, podríamos hacerlo dentro de esta función, pero eso es todo lo que haremos por ahora.

Sin embargo, heredar el constructor no es suficiente; también necesitamos heredar el prototipo. Aquí es donde entra en juego el módulo util.

1
2
    util.inherits(UserList, EventEmitter);

Esto agregará todo lo que está en EventEmitter.prototype a UserList.prototype; ahora, nuestras instancias UserList tendrán todos los métodos de una instancia de EventEmitter. Pero queremos añadir un poco más, por supuesto. Añadiremos un método save, para permitirnos agregar nuevos usuarios.

1
2
    UserList.prototype.save = function (obj) {
3
        obj.id = id++;
4
        database.users.push(obj);
5
        this.emit("saved-user", obj);  
6
    };

Este método toma un objeto para guardar en nuestra "database": agrega un id y lo inserta en la matriz de los usuarios. Luego, emite el evento "saved-user" y pasa el objeto como datos. Si se tratara de una base de datos real, guardarla probablemente sería una tarea asincrónica, lo que significa que para trabajar con el registro guardado tendríamos que aceptar una devolución de llamada. La alternativa a esto es emitir un evento, como lo estamos haciendo. Ahora, si queremos hacer algo con el registro guardado, podemos escuchar el evento. Lo haremos en un segundo. Cerremos la UserList

1
2
    UserList.prototype.all = function () {
3
        return database.users;
4
    };
5
6
    module.exports = UserList;

Agregué un método más: uno simple que devuelve a todos los usuarios. Luego, asignamos UserList a module.exports.

Ahora, veamos esto en uso; en otro archivo, digamos test.js. Agrega lo siguiente:

1
2
    var UserList = require("./userlist");
3
    var users = new UserList();
4
5
    users.on("saved-user", function (user) {
6
        console.log("saved: " + user.name + " (" + user.id + ")");
7
    });
8
9
    users.save({ name: "Jane Doe", occupation: "manager" });
10
    users.save({ name: "John Jacob", occupation: "developer" });

Después de requerir nuestro nuevo módulo y crear una instancia del mismo, escuchamos el evento "saved-user". Entonces, podemos proseguir y conservar algunos usuarios. Cuando ejecutemos esto, verás que recibimos dos mensajes, imprimiendo los nombres y los identificadores de los registros que guardamos.

1
2
    saved: Jane Doe (4)
3
    saved: John Jacob (5)

Por supuesto, esto podría funcionar al revés: podríamos estar usando el método on desde el interior de nuestra clase y el método emit desde fuera, o tanto por dentro como por fuera. Pero este es un buen ejemplo de cómo se podría hacer.


Conclusión

Así es como funciona la clase EventEmitter de Node. A continuación, encontrarás los enlaces a la documentación de Node para algunas de las cosas de las que hemos estado hablando.