Sebastian Gomez
Implementando Observables a partir de Eventos en JS
En este post vamos a explorar la implementación de observables y cómo la programación reactiva y funcional está cambiando la forma en que abordamos el código. Prepárate para un viaje lleno de conocimientos y diversión.
Programación reactiva y funcional
La programación reactiva y funcional es una tendencia en crecimiento en el mundo del desarrollo, pero aún no es ampliamente conocida, especialmente entre las personas que recién empiezan. En este post vamos a explorar la definición más pura de un observable y cómo implementar la función generadora de observables más común, tal como lo hace RxJS, para comprender el concepto en profundidad.
Entendiendo los observables
La mejor manera de acercarse a los observables es entender que son la combinación de dos patrones de diseño en JavaScript: el patrón iterador, que explicamos en este post sobre el patrón iterador, y el patrón observer, que explicamos en este post sobre el patrón observer.
Nota: verifica que ambos enlaces resuelvan a los posts hermanos correctos antes de publicar. El post del patrón iterador existe en este sitio; confirma la URL exacta del post del patrón observer.
Después de leer estos dos patrones y tener una idea en mente de lo que son, un observable no es más que una función en JavaScript con otra función en su interior que retorna un objeto (JSON) con tres funciones: onNext, onError y onCompleted.
Implementando un observable en JavaScript
A continuación se muestra cómo implementar un observable en JavaScript con la sintaxis clásica de prototipos (ES5):
// Estilo clásico de prototipos (ES5)
// Esta versión usa funciones constructoras y prototipos, el estilo anterior a las clases de ES6.
function Observable(forEach) {
// Definimos la función constructora 'Observable' que toma un argumento 'forEach' y crea un nuevo objeto Observable.
this._forEach = forEach;
// Asignamos la función forEach recibida como argumento a la propiedad '_forEach' del objeto Observable.
}
Observable.prototype = {
// Definimos las propiedades del objeto 'prototype' de la función constructora 'Observable'.
forEach: function (onNext, onError, onCompleted) {
// Declaramos un método 'forEach' en el prototipo de 'Observable' que toma tres argumentos: onNext, onError y onCompleted.
if (typeof onNext === "function") {
// Si el primer argumento 'onNext' es una función, entonces...
return this._forEach({
// ...ejecutamos la función almacenada en '_forEach' con un objeto que contiene las propiedades y sus valores.
onNext: onNext,
// 'onNext' se asigna al valor pasado como primer argumento.
onError: onError || function () {},
// Si se proporciona 'onError', se asigna a la propiedad 'onError'. De lo contrario, se asigna una función vacía.
onCompleted: onCompleted || function () {},
// Si se proporciona 'onCompleted', se asigna a la propiedad 'onCompleted'. De lo contrario, se asigna una función vacía.
});
} else {
// Si el primer argumento 'onNext' no es una función, entonces...
return this._forEach(onNext);
// ...ejecutamos la función almacenada en '_forEach' con el primer argumento 'onNext' como parámetro.
}
},
};Como puedes ver, es una función llamada Observable que recibe como parámetro una función llamada forEach. Esto tiene todo que ver con la función forEach que usamos con los arreglos en JavaScript. Esta función se implementa de manera interna en el Observable para hacerla propia de la definición del observable.
Capturando eventos con observables
Imagina que quieres mostrar un console.log("Hizo click"); cada vez que una persona hace clic en un botón de tu sitio web. Para ello vamos a crear una función estática dentro de la definición de Observable que creamos antes, que convertirá un evento del DOM en un observable:
Observable.fromEvent = function (dom, eventName) {
// Agregamos un método estático 'fromEvent' al objeto 'Observable'.
// Este método toma dos argumentos: un elemento del DOM 'dom' y el nombre de un evento 'eventName'.
return new Observable(function forEach(observer) {
// La función crea y retorna un nuevo objeto 'Observable'.
// La función 'forEach' que se pasa como argumento al constructor toma un 'observer' como parámetro.
var handler = (e) => observer.onNext(e);
// Creamos un 'handler' que es una función de flecha que recibe un evento 'e' y llama al método 'onNext' del 'observer', pasándole 'e'.
dom.addEventListener(eventName, handler);
// Registramos el 'handler' en el elemento del DOM 'dom' para el evento especificado en 'eventName'.
return {
// Retornamos un objeto que tiene una única propiedad 'dispose'.
dispose: () => {
// La propiedad 'dispose' es una función de flecha que...
dom.removeEventListener(eventName, handler);
// ...elimina el 'handler' previamente registrado en el elemento del DOM 'dom' para el evento especificado en 'eventName'.
},
};
});
};La función fromEvent recibe un elemento del DOM y el nombre del evento que queremos rastrear, y retorna un observable que nos devuelve cada evento de clic a medida que ocurre. Veamos cómo se usa:
const button = document.getElementById("button");
// Declaramos una constante 'button' y le asignamos el elemento del DOM con el ID "button".
const clicks = Observable.fromEvent(button, "click");
// Declaramos una constante 'clicks' y le asignamos el resultado de llamar al método estático 'fromEvent' de 'Observable',
// pasándole el elemento del DOM 'button' y el nombre del evento "click". Esto crea un objeto 'Observable' que escucha el
// evento de clic en el botón.
const clicksSubscription = clicks.forEach((event) => {
// Declaramos una constante 'clicksSubscription' y le asignamos el resultado de llamar al método 'forEach' en el objeto
// 'clicks'. Esta función toma una función de flecha como argumento que recibe un evento 'event'.
console.log("El usuario hizo click", event);
// Dentro de la función de flecha, registramos en la consola un mensaje "El usuario hizo click" seguido del objeto 'event'.
});Ahora vamos a ver cómo podemos crear observables a partir de eventos usando la sintaxis moderna de clases de ES6 (ES2015), las diferencias con el estilo anterior y cómo suscribirnos y cancelar la suscripción a los observables. Acompáñanos en este emocionante viaje.
Usando clases de ES6 (ES2015) para observables
Al usar la sintaxis de clases de ES6, lo primero que debemos tener en cuenta es que la función forEach se reemplaza por convención por la función subscribe. Además, utilizaremos las palabras reservadas class y static para crear nuestra implementación de observable. Veamos cómo implementamos la definición pura de observable con clases:
class Observable {
// Declaramos una clase 'Observable'.
constructor(subscribe) {
// Definimos el constructor de la clase, que toma un argumento 'subscribe'.
this._subscribe = subscribe;
// Asignamos la función 'subscribe' recibida como argumento a la propiedad '_subscribe' de la instancia del objeto Observable.
}
subscribe(observer) {
// Declaramos un método 'subscribe' en la clase 'Observable' que toma un argumento 'observer'.
return this._subscribe(observer);
// Ejecutamos la función almacenada en la propiedad '_subscribe' de la instancia del objeto Observable,
// pasándole el argumento 'observer' y retornando el resultado.
}
}Gracias a la definición de clase y constructor, la implementación del observable es más corta. Además, en esta nueva definición no nos importa cómo el observador (observer) recibe las funciones onNext, onError y onCompleted (que por convención se cambian a next, error y complete).
Implementando fromEvent con clases de ES6 (ES2015)
Ahora veamos cómo implementar la función fromEvent para crear observables a partir de eventos usando clases:
class Observable {
// Declaramos una clase 'Observable'.
// ...
// Aquí se deben incluir el constructor y otros métodos que no se muestran en este fragmento de código.
static fromEvent(domElement, eventName) {
// Declaramos un método estático 'fromEvent' en la clase 'Observable' que toma dos argumentos: un elemento del DOM 'domElement' y el nombre de un evento 'eventName'.
return new Observable(function subscribe(observer) {
// La función crea y retorna un nuevo objeto 'Observable'.
// La función 'subscribe' que se pasa como argumento al constructor toma un 'observer' como parámetro.
const handler = (ev) => { observer.next(ev) };
// Creamos un 'handler' que es una función de flecha que recibe un evento 'ev' y llama al método 'next' del 'observer', pasándole 'ev'.
domElement.addEventListener(eventName, handler);
// Registramos el 'handler' en el elemento del DOM 'domElement' para el evento especificado en 'eventName'.
return {
// Retornamos un objeto que tiene una única propiedad 'unsubscribe'.
unsubscribe() {
// La propiedad 'unsubscribe' es una función que...
domElement.removeEventListener(eventName, handler);
// ...elimina el 'handler' previamente registrado en el elemento del DOM 'domElement' para el evento especificado en 'eventName'.
}
}
});
}
}Ejecución de observables
La ejecución del observable se realiza de manera similar al estilo anterior, pero en lugar de usar la función forEach, usamos la función subscribe:
// Creamos una constante llamada 'button' que almacena el elemento HTML con el ID "button"
const button = document.getElementById("button");
// Creamos una constante llamada 'clicks' que usa nuestra propia implementación de Observable para observar el evento 'click' del elemento HTML almacenado en la constante 'button'
const clicks = Observable.fromEvent(button, "click");
// Creamos una constante llamada 'clicksSubscription' que suscribe una función de devolución de llamada que se ejecuta cada vez que se detecta un evento 'click'
const clicksSubscription = clicks.subscribe({
// La función 'next' es llamada cada vez que se detecta un evento 'click' y muestra un mensaje de registro en la consola
next(e) {
console.log("Click in the button");
},
});Aunque hay diferencias en la implementación de la definición pura más simple de un observable entre el estilo anterior y el moderno, la invocación y ejecución del observable es muy similar en ambos casos.
Observables fríos
Los observables son "fríos" o "perezosos" por naturaleza, lo que significa que no hacen nada a menos que alguien los esté escuchando. Para cancelar la suscripción a un observable, puedes usar el método dispose (estilo anterior) o unsubscribe (estilo moderno) que viene con la suscripción:
// Estándar antiguo
clicksSubscription.dispose();
// Estándar nuevo
clicksSubscription.unsubscribe();Conclusiones
La implementación de observables con clases de ES6 (ES2015) es más corta y fácil de entender gracias al uso de class y constructor.
Con la sintaxis de clases, la función forEach se reemplaza por la función subscribe, y las funciones onNext, onError y onCompleted se cambian a next, error y complete.
Los observables son "fríos" o "perezosos" y solo se ejecutan cuando alguien los está escuchando. Para cancelar la suscripción, se utiliza el método dispose (estilo anterior) o unsubscribe (estilo moderno).
Ejercicios propuestos
- Crea un observable que muestre un mensaje cada vez que la persona mueva el mouse sobre un elemento específico en la página.
- Implementa un observable que se ejecute cuando se presione una tecla en un campo de texto, y filtre solo los eventos de las teclas alfabéticas (a z).
- Diseña un observable que combine eventos de múltiples elementos en la página y realice acciones en función de la secuencia de eventos.
Resumen en 3 puntos
- Las clases de ES6 (ES2015) facilitan la creación de observables gracias a la definición de clases y constructores, y la función
subscribereemplaza la funciónforEach. - La función
fromEventnos permite crear observables a partir de eventos del DOM, facilitando la manipulación de eventos en aplicaciones web. - Los observables son fríos y solo se ejecutan cuando tienen al menos un observador, lo que nos da un mayor control sobre cuándo se ejecutan y cuándo se detienen.
Espero que este post te haya sido útil y te haya ayudado a comprender la naturaleza de los observables en JavaScript. No dudes en dejar un comentario si tienes alguna pregunta, sugerencia o si deseas agregar alguna otra funcionalidad. Si te gustó, comparte este post en tus redes sociales usando los enlaces de aquí abajo. Hasta la próxima.
Sebastian Gomez
Creador de contenido principalmente acerca de tecnología.