Reactividad sin Frameworks: Observables desde Cero con JavaScript Puro 🚀
Este tutorial te guiará paso a paso en la creación de tu propia librería de observables en JavaScript, permitiéndote manejar flujos de datos y eventos de forma reactiva. Descubrirás cómo suscribirte, desuscribirte, transformar datos y encadenar operaciones, sentando las bases de la programación reactiva.
La programación reactiva ha ganado una enorme popularidad, especialmente con el auge de librerías como RxJS. Pero, ¿qué pasaría si pudieras entender y construir los conceptos fundamentales de los observables desde cero, utilizando solo JavaScript puro? Este tutorial te sumergirá en el corazón de la reactividad, desmitificando los observables y empoderándote para crear tus propios flujos de datos y eventos.
Olvídate por un momento de los frameworks y prepárate para construir una base sólida en un concepto que transformará la forma en que piensas sobre el manejo de la asincronía y el estado en tus aplicaciones.
¿Qué son los Observables? 🤔
En su esencia, un Observable es una fuente de datos o eventos que emite valores a lo largo del tiempo. Piensa en ellos como colecciones que evolucionan asincrónicamente. A diferencia de las promesas, que resuelven un único valor en el futuro, los observables pueden emitir múltiples valores a lo largo de su ciclo de vida, incluyendo notificaciones de errores y de completitud. Son ideales para manejar:
- Eventos de UI (clics, entradas de teclado).
- Peticiones HTTP que devuelven múltiples respuestas (ej. WebSockets).
- Animaciones y secuencias de tiempo.
- Cualquier flujo de datos asíncrono o sincrónico.
La clave de los observables es su naturaleza "pull" (el suscriptor tira de los valores cuando están disponibles) y "push" (el observable empuja los valores al suscriptor). Esto permite un control más granular sobre cómo y cuándo se consumen los datos.
Arquitectura Básica: Observable y Subscription 🛠️
Para construir nuestra librería, necesitaremos dos clases principales:
Observable: La clase que representa el flujo de datos. Contendrá la lógica para emitir valores.Subscription: Un objeto que representa la conexión activa entre un observable y un observador. Permite gestionar el ciclo de vida de la suscripción (ej. desuscribirse).
También necesitaremos una forma de definir un Observer, que es el objeto con los métodos (next, error, complete) que el observable llamará para emitir valores.
La Clase Observable
Comencemos definiendo nuestra clase Observable. Su constructor recibirá una función _subscribe que define cómo el observable producirá sus valores.
class Observable {
constructor(producer) {
// El productor es una función que define cómo el observable emitirá valores.
// Recibirá un observador como argumento.
this.producer = producer;
}
subscribe(observerOrNext, error, complete) {
// Convierte el argumento a un objeto observer completo si es solo una función 'next'
const observer = typeof observerOrNext === 'function'
? { next: observerOrNext, error: error || (() => {}), complete: complete || (() => {}) }
: observerOrNext;
// Llama al productor para empezar a emitir valores. El productor
// debe devolver una función para desuscribirse.
const unsubscribeFn = this.producer(observer);
// Retorna una instancia de Subscription para gestionar la suscripción.
return new Subscription(unsubscribeFn);
}
}
La Clase Subscription
La clase Subscription es esencial para el manejo de recursos. Cuando nos suscribimos a un observable, obtenemos una Subscription que nos permite cancelar el flujo de datos y liberar recursos.
class Subscription {
constructor(unsubscribe) {
this.unsubscribeFn = unsubscribe;
this.closed = false;
}
unsubscribe() {
if (!this.closed) {
if (typeof this.unsubscribeFn === 'function') {
this.unsubscribeFn();
}
this.closed = true;
}
}
}
Creando Nuestro Primer Observable ✨
Ahora que tenemos la estructura básica, creemos un observable simple que emita algunos números.
const myObservable = new Observable(observer => {
let count = 0;
const intervalId = setInterval(() => {
if (count < 3) {
observer.next(count++); // Emite el valor
} else {
observer.complete(); // Indica que el flujo ha terminado
clearInterval(intervalId);
}
}, 1000);
// Función de limpieza para cuando se desuscribe
return () => {
console.log('Desuscripción: Limpiando el intervalo.');
clearInterval(intervalId);
};
});
console.log('Antes de la suscripción...');
const mySubscription = myObservable.subscribe({
next: value => console.log('Recibido:', value),
error: err => console.error('Error:', err),
complete: () => console.log('Completado!')
});
console.log('Después de la suscripción...');
// Podríamos desuscribirnos manualmente después de un tiempo
// setTimeout(() => {
// mySubscription.unsubscribe();
// console.log('Suscripción deshabilitada manualmente.');
// }, 2500);
Explicación del código:
- El
producer(la función que pasamos al constructor deObservable) recibe unobserver. - Dentro del
producer, usamossetIntervalpara emitir valores0, 1, 2cada segundo usandoobserver.next(). - Después de emitir
2, llamamos aobserver.complete()para indicar que el flujo ha terminado, y limpiamos elsetInterval. - La función retornada por el
producer(return () => {...}) es la lógica que se ejecutará cuando se llame amySubscription.unsubscribe().
Métodos de Creación Comunes 🏭
Extender nuestra clase Observable con métodos estáticos para crear observables de diferentes fuentes es muy útil. Esto imita la API de librerías como RxJS.
Observable.of(...values)
Crea un observable que emite una secuencia de valores sincrónicamente y luego se completa.
class Observable {
// ... (constructor y subscribe como antes)
static of(...values) {
return new Observable(observer => {
for (const value of values) {
observer.next(value);
}
observer.complete();
return () => {}; // No hay limpieza asíncrona necesaria
});
}
}
const obsOf = Observable.of(10, 20, 30);
obsOf.subscribe(val => console.log('Observable.of:', val));
// Salida:
// Observable.of: 10
// Observable.of: 20
// Observable.of: 30
// Completado!
Observable.fromEvent(element, eventName)
Crea un observable a partir de eventos DOM.
class Observable {
// ... (constructor y subscribe como antes)
static fromEvent(element, eventName) {
return new Observable(observer => {
const handler = event => observer.next(event);
element.addEventListener(eventName, handler);
return () => {
console.log(`Eliminando listener para ${eventName}`);
element.removeEventListener(eventName, handler);
};
});
}
}
// Ejemplo de uso con un botón (necesitarías un elemento HTML real)
// <button id="myButton">Haz clic</button>
// const button = document.getElementById('myButton');
// if (button) {
// const click$ = Observable.fromEvent(button, 'click');
// const clickSubscription = click$.subscribe(event => console.log('¡Clic!', event.target.id));
// // Desuscribirse después de 5 segundos
// // setTimeout(() => {
// // clickSubscription.unsubscribe();
// // }, 5000);
// }
Observable.interval(delay)
Emite números secuenciales en intervalos de tiempo.
class Observable {
// ... (constructor y subscribe como antes)
static interval(delay) {
return new Observable(observer => {
let count = 0;
const intervalId = setInterval(() => {
observer.next(count++);
}, delay);
return () => {
console.log(`Limpiando intervalo de ${delay}ms`);
clearInterval(intervalId);
};
});
}
}
const interval$ = Observable.interval(1000);
const intervalSubscription = interval$.subscribe(num => console.log('Intervalo:', num));
setTimeout(() => {
intervalSubscription.unsubscribe();
console.log('Intervalo deshabilitado.');
}, 4500);
// Salida:
// Intervalo: 0 (después de 1s)
// Intervalo: 1 (después de 2s)
// Intervalo: 2 (después de 3s)
// Intervalo: 3 (después de 4s)
// Limpiando intervalo de 1000ms
// Intervalo deshabilitado.
Operadores: Transformando Flujos de Datos 🔄
Los operadores son funciones que toman un observable como entrada y devuelven un nuevo observable modificado. Permiten transformar, filtrar o combinar los valores emitidos. Añadiremos métodos a nuestra clase Observable para esto.
Operador map(projectionFn)
Transforma cada valor emitido por el observable utilizando una función de proyección.
class Observable {
// ... (constructor, subscribe, métodos estáticos como antes)
map(projectionFn) {
return new Observable(observer => {
const subscription = this.subscribe({
next: value => {
try {
observer.next(projectionFn(value));
} catch (err) {
observer.error(err);
}
},
error: err => observer.error(err),
complete: () => observer.complete()
});
return () => subscription.unsubscribe();
});
}
}
const mappedInterval$ = Observable.interval(1000).map(val => val * 2);
const mappedSubscription = mappedInterval$.subscribe(val => console.log('Mapped:', val));
setTimeout(() => {
mappedSubscription.unsubscribe();
}, 3500);
// Salida:
// Mapped: 0
// Mapped: 2
// Mapped: 4
// Limpiando intervalo de 1000ms
// Intervalo deshabilitado.
Operador filter(predicateFn)
Filtra los valores emitidos, dejando pasar solo aquellos que cumplen una condición.
class Observable {
// ... (constructor, subscribe, métodos estáticos, map como antes)
filter(predicateFn) {
return new Observable(observer => {
const subscription = this.subscribe({
next: value => {
try {
if (predicateFn(value)) {
observer.next(value);
}
} catch (err) {
observer.error(err);
}
},
error: err => observer.error(err),
complete: () => observer.complete()
});
return () => subscription.unsubscribe();
});
}
}
const filteredInterval$ = Observable.interval(1000)
.filter(val => val % 2 === 0)
.map(val => 'Par: ' + val);
const filteredSubscription = filteredInterval$.subscribe(val => console.log('Filtered:', val));
setTimeout(() => {
filteredSubscription.unsubscribe();
}, 6500);
// Salida:
// Filtered: Par: 0
// Filtered: Par: 2
// Filtered: Par: 4
// Limpiando intervalo de 1000ms
// Intervalo deshabilitado.
Encadenamiento de Operadores
La belleza de los observables con operadores es su capacidad de encadenamiento. Cada operador devuelve un nuevo observable, permitiendo construir flujos complejos de forma declarativa.
const complexFlow$ = Observable.interval(500) // Emite cada 0.5s
.filter(val => val % 3 === 0) // Solo múltiplos de 3
.map(val => `Múltiplo de 3: ${val}`);
const complexSubscription = complexFlow$.subscribe({
next: data => console.log(data),
complete: () => console.log('Flujo complejo completado!')
});
setTimeout(() => {
complexSubscription.unsubscribe();
console.log('Suscripción compleja terminada.');
}, 7000);
// Salida (ejemplo):
// Múltiplo de 3: 0 (0.5s)
// Múltiplo de 3: 3 (2s)
// Múltiplo de 3: 6 (3.5s)
// Múltiplo de 3: 9 (5s)
// Múltiplo de 3: 12 (6.5s)
// Limpiando intervalo de 500ms
// Suscripción compleja terminada.
Manejo de Errores y Completitud ⚠️
Un observable puede terminar de dos maneras: completándose (complete()) o con un error (error(err)). Una vez que se llama a complete() o error(), el observable deja de emitir valores y la suscripción se considera finalizada.
Ejemplo con Errores
const errorProneObservable = new Observable(observer => {
let count = 0;
const intervalId = setInterval(() => {
if (count === 2) {
observer.error(new Error('¡Algo salió mal en la emisión 2!'));
clearInterval(intervalId);
} else {
observer.next(count++);
}
}, 500);
return () => {
console.log('Limpiando después de error o completitud.');
clearInterval(intervalId);
};
});
errorProneObservable.subscribe({
next: val => console.log('Valor:', val),
error: err => console.error('Se capturó un error:', err.message),
complete: () => console.log('¡Nunca se completará si hay error!')
});
// Salida:
// Valor: 0
// Valor: 1
// Se capturó un error: ¡Algo salió mal en la emisión 2!
// Limpiando después de error o completitud.
Es fundamental que tu producer siempre limpie sus recursos cuando se llama a complete o error, o cuando la suscripción es deshabilitada manualmente.
Ventajas de la Programación Reactiva con Observables ✅
- Manejo Unificado de Asincronía: Proporciona una forma consistente de tratar eventos, peticiones HTTP, temporizadores, etc.
- Composición Poderosa: Los operadores permiten transformar y combinar flujos de datos complejos de manera declarativa y legible.
- Manejo de Errores Centralizado: Los errores se propagan a través del flujo y pueden ser manejados en un solo lugar.
- Cancelación de Suscripciones: Las
Subscriptions ofrecen un mecanismo claro para liberar recursos y detener flujos innecesarios. - Flujo de Datos Predictible: Facilita la comprensión de cómo los datos se mueven y transforman a través de la aplicación.
Comparación con Promesas
Aunque ambos manejan asincronía, los observables y las promesas tienen propósitos diferentes:
| Característica | Promesas | Observables |
|---|---|---|
| Valores Emitidos | Un único valor futuro | Cero a muchos valores a lo largo del tiempo |
| Naturaleza | eager (ansioso) – empiezan a ejecutar inmediatamente. | lazy (perezoso) – solo empiezan a ejecutar cuando hay un suscriptor. |
| Cancelación | No cancelables de forma nativa | Cancelables a través de unsubscribe() |
| Composición | then(), catch(), finally() | map(), filter(), merge(), debounce(), take(), etc. (operadores) |
| Sincronicidad | Siempre asíncronas | Pueden ser síncronos o asíncronos |
Más allá de lo Básico: Próximos Pasos 🚀
Esta implementación es una base sólida, pero puedes expandirla significativamente:
- Más Operadores: Implementa operadores populares como
merge,combineLatest,debounceTime,take,distinctUntilChanged. Cada operador es un ejercicio fascinante en la composición de flujos. - Scheduler: Introduce la idea de un scheduler para controlar cuándo se ejecutan las emisiones (ej.
setTimeout,requestAnimationFrame). - Subjects: Implementa
Subjects para crear observables que también sean observadores, permitiendo la comunicación multicast (varios observadores a una única fuente). - Encadenamiento de
unsubscribe: La claseSubscriptionpuede contener múltiples funciones de limpieza, útiles cuando un observable interno crea sus propias suscripciones.
Conclusión ✨
Has construido los fundamentos de tu propia librería de programación reactiva con observables en JavaScript puro. Entender estos conceptos desde cero no solo te da una visión profunda de cómo funcionan frameworks y librerías, sino que también te proporciona una herramienta poderosa para manejar la complejidad asíncrona en cualquier proyecto. La reactividad es un paradigma que, una vez comprendido, simplifica enormemente el desarrollo de aplicaciones dinámicas y con uso intensivo de datos.
¡Felicidades por dominar la reactividad desde sus cimientos! ¡Ahora puedes aplicar este conocimiento para crear flujos de datos más robustos y predecibles en tus aplicaciones!
Tutoriales relacionados
- Patrones de Módulos en JavaScript: Organizador tu Código como un Pro 🚀intermediate18 min
- Explorando los Iteradores y Generadores en JavaScript: ¡Más allá de los bucles tradicionales!intermediate15 min
- Callbacks, Promesas y Async/Await: Gestión de Asincronía en JavaScriptintermediate18 min
- Proxy y Reflect en JavaScript: Intercepta Operaciones de Objetos con Poder 🚀advanced18 min
- Manipulación del DOM con JavaScript: Interactividad Dinámica en tu Webintermediate20 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!