FRÍO es cuando el Observable crea el producer
// FRÍO
var cold = new Observable((observer) => {
var producer = new Producer();
// El observer escuchará al producer aquí
});
CALIENTE es cuando el Observable engloba al producer
// CALIENTE
var producer = new Producer();
var hot = new Observable((observer) => {
// El observer escuchará al producer aquí
});
Profundizando en lo que sucede…
El objetivo de mi último artículo Learning Observable By Building Observable era, ante todo, ilustrar que los Observables son simplemente funciones, poder desmitificar a los Observables en sí mismos. Sin embargo, no profundiza realmente en una de las características de los Observables que más confunde a la gente: El concepto de “caliente” y “frío”.
¡Los Observables solo son funciones!
Los Observables son funciones que vinculan un observer con un producer. Eso es todo. No tienen por qué establecer el producer, pueden limitarse a preparar al observer para que escuche al producer, y además suelen devolver un mecanismo de teardown/demolición para eliminar ese listener. La suscripción es el acto de “llamar” al Observable, como una función, y pasársela a un observer.
Qué es un “Producer”
Un producer es la fuente de valores de un Observable. Puede ser un web socket, pueden ser eventos del DOM, puede ser un iterador, o algo iterando sobre un array. Básicamente, es cualquier cosa que utilicemos para obtener valores y pasárselos a observer.next(valor)`
.
Observables Fríos: los producers se crean *dentro*
Un Observable es “frío” si el producer subyacente se crea y activa durante la suscripción. Eso quiere decir que si los Observables son funciones, entonces el producer se crea y activa al llamar a esa función.
- Crea el producer.
- Activa el producer.
- Empieza a escuchar al producer.
- Unicast/monoemisión
El siguiente ejemplo es de un Observable “frío”, ya que el WebSocket se crea y empieza a ser escuchado dentro de la función subscriber, que es llamada cuando nos suscribimos al Observable:
const source = new Observable((observer) => {
const socket = new WebSocket('ws://algunaurl');
socket.addEventListener('message', (e) => observer.next(e));
return () => socket.close();
});
Por tanto, cualquiera que se suscriba a source
obtendrá su propia instancia de WebSocket, y al cancelar la suscripción, el socket se cerrará mediante close()
. Esto quiere decir que nuestro Observable es unicast/monoemisión, ya que el producer solamente puede enviar valores a un observer. Aquí tenéis un JSBin básico ilustrando este concepto.
Observables Calientes: los producers se crean *fuera*
Un Observable es “caliente” si el producer subyacente se crea o activa fuera de la suscripción.¹
- Comparte la referencia a un producer.
- Empieza a escuchar al producer.
- Multicast/multiemisión (normalmente²).
Si modificamos el ejemplo anteriormente y sacamos la creación del WebSocket fuera de nuestro Observable, este se convertiría en “caliente”:
const socket = new WebSocket('ws://algunaurl');const source = new Observable((observer) => {
socket.addEventListener('message', (e) => observer.next(e));
});
Ahora, cualquiera que se suscriba asource
compartirá la misma instancia de WebSocket. El Observable es ahora multicast para todos los subscribers. Sin embargo, nos encontramos con un pequeño problema: nuestro Observable carece de la lógica de teardown/demolición del socket. Esto quiere decir que aunque se dé un error, el Observable se complete o cancelemos la suscripción, el socket no se cerrará. Por tanto, lo que realmente necesitamos es convertir nuestro Observable “frío” en uno “caliente”. Aquí tenéis un JSBin básico ilustrando este concepto.
Por qué construir un Observable “Caliente”
Del primer ejemplo de un Observable frío, podemos sacar la conclusión de que trabajar exclusivamente con Observables fríos nos puede ocasionar problemas. Por un lado, si nos suscribimos a un Observable que crea un recurso escaso, como una conexión web socket, más de una vez, estaremos recreando esta conexión una y otra vez. De hecho, es muy fácil crear más de una suscripción a un Observable sin darnos cuenta. Por ejemplo, si queremos filtrar todos los valores pares e impares de una suscripción a un web socket, crearemos dos suscripciones, de la siguiente manera:
source.filter(x => x % 2 === 0)
.subscribe(x => console.log('par', x));source.filter(x => x % 2 === 1)
.subscribe(x => console.log('impar', x));
Subjects de Rx
Antes de que podamos convertir nuestro Observable “frío” en uno “caliente”, tenemos que hablar de un tipo nuevo: el Subject de Rx. Tiene las siguientes características:
- Es un Observable. Tiene la forma de un Observable, y los mismos operadores.
- Es un observer. Pasa la prueba del duck-typing como observer. Cuando nos sucribimos a un Subject como Observable, emitirá cualquier valor que le proporcionemos con
next()
como un observer. - Es multicast. Todos los observers que se le pasen mediante
subscribe()
se añaden a una lista de observers interna. - Cuando se termina, se termina. Los Subjects no pueden reutilizarse después de cancelar la suscripción, de que hayan completado, o de que haya habido algún error.
- Pasa valores a través de sí mismo. Realmente, estamos replanteando #2. Si le pasamos un valor mediante
next()
, saldrá por el “lado Observable” de sí mismo.
Un Subject de Rx es conocido como subject por la característica #3. Los Subjects en el patrón Observer de la Banda de los Cuatro (Gang of Four) son clases con un método addObserver
, normalmente. En este caso, nuestro método addObserver
es subscribe
. Aquí tenéis un JSBin básico ilustrando este concepto.
Convirtiendo un Observable Frío a Caliente
Armados con nuestro Subject Rx, podemos utilizar un poco de programación funcional para hacer que cualquier Observable “frío” pase a ser “caliente”:
function makeHot(cold) {
const subject = new Subject();
cold.subscribe(subject);
return new Observable((observer) => subject.subscribe(observer));
}
Nuestro método makeHot
cogerá cualquier Observable frío y lo convertirá en caliente, mediante la creación de un Subject que es compartido mediante el Observable resultando. Podemos ver el método en acción en este JSBin.
Sin embargo, seguimos teniendo un pequeño problema: no estamos haciendo un seguimiento de nuestra suscripción a la fuente, así que, ¿Cómo vamos a poder demolerla cuando nos haga falta? Podemos añadir reference counting/recuento de referencias para solucionarlo:
function makeHotRefCounted(cold) {
const subject = new Subject();
const mainSub = cold.subscribe(subject);
let refs = 0;
return new Observable((observer) => {
refs++;
let sub = subject.subscribe(observer);
return () => {
refs--;
if (refs === 0) mainSub.unsubscribe();
sub.unsubscribe();
};
});
}
Ahora tenemos un Observable caliente, y cuando todas las suscripciones a él se terminan, la variable refs
, que utilizamos para mantener el recuento de las referencias, valdrá cero, y cancelaremos la suscripción a nuestro Observable fuente frío. Podemos ver el método en acción en este JSBin.
En RxJS, usamos publish() o share()
Probablemente no deberíamos usar ninguna de las funciones makeHot
anteriores, y utilizar operadores como publish()
o share()
en su lugar. Hay muchas formas de convertir un Observable frío a caliente, y en Rx hay técnicas eficaces y concisas de conseguirlo. Se podría escribir un artículo entero sobre todos los operadores que se utilizan para ello en Rx, pero ese no es el objetivo ahora. El verdadero objetivo es solidificar la idea de lo que los conceptos de “caliente” y “frío” realmente significan.
En RxJS 4, el operador share()
crea un Observable caliente, con recuento de referencias, que puede reintentarse en el caso de un fallo, o repetirse en un caso de éxito. Dado que los Subjects no se pueden reutilizar una vez que se haya producido un error, se hayan completado, o cancelado la suscripción, el operador share()
reciclará Subjects muertos para permitir la resuscripción al Observable resultante.
Aquí tenéis un JSBin demostrando el uso de share()
para hacer que una fuente sea caliente en RxJS 5, y mostrando que puede reintentarse.
El Observable “Templado”
Dado todo lo anterior, podemos ver cómo un Observable, ya que es simplemente una función, puede ser tanto “caliente” como “frío”. ¿Quizá observe dos producers, uno que crea y otro que engloba? Probablemente sea mal karma, pero hay algunos casos muy concretos donde quizá pueda ser necesario hacerlo. Por ejemplo, un web socket multiplex: hay que compartir el socket, pero enviando su propia suscripción, y filtrando el flujo de datos.
Tanto “Caliente” como “Frío”, dependen del producer
Si estamos englobando una referencia compartida a un producer en nuestro Observable, es “caliente”. Si estamos creando un producer nuevo en nuestro Observable, es “frío”. Si hacemos ambas cosas… ¿Qué tenemos? Un Observable “templado”, supongo.
NOTAS
¹ Es un poco extraño el decir que el producer se “activa” dentro de la suscripción, pero no se “crea” hasta más tarde, pero, con proxies, esto podría ser posible. Normalmente, los producers de los Observables “calientes” se crean y activan fuera de la suscripción.
² Los Observables calientes suelen ser multicast/multiemisión, pero podrían estar escuchando a un producer que solo permite tener un listener a la vez. Las bases para poder llamarlo “multicast” en este caso, no están demasiado claras.
¿Quieres saber más? ¡Imparto workshops de RxJS workshops en RxWorkshop.com!
Nota de la editora: Este artículo ha sido publicado originalmente por Ben Lesh en medium: Hot vs Cold Observables
Traducción por Estefanía García Gallardo.