Programación asíncrona en JavaScript (Callback, Promise, RxJs)

Hola a todos. En contacto Omelnitsky Sergey. No hace mucho, organicé una transmisión sobre programación reactiva, donde hablé sobre la asincronía en JavaScript. Hoy me gustaría resumir este material.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

Pero antes de comenzar con el material principal, necesitamos hacer una introducción. Entonces, comencemos con las definiciones: ¿qué son la pila y la cola?

Pila es una colección cuyos elementos se recuperan en una base LIFO de "último en entrar, primero en salir"

Cola es una colección cuyos elementos se obtienen según el principio (“primero en entrar, primero en salir” FIFO

Bien, continuemos.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

JavaScript es un lenguaje de programación de un solo subproceso. Esto significa que solo tiene un hilo de ejecución y una pila donde las funciones se ponen en cola para su ejecución. Por lo tanto, JavaScript solo puede realizar una operación a la vez, mientras que otras operaciones esperarán su turno en la pila hasta que sean llamadas.

pila de llamadas es una estructura de datos que, en términos simples, registra información sobre el lugar del programa en el que nos encontramos. Si saltamos a una función, empujamos su entrada a la parte superior de la pila. Cuando regresamos de una función, extraemos el elemento superior de la pila y terminamos desde donde llamamos a esta función. Eso es todo lo que la pila puede hacer. Y ahora una pregunta muy interesante. Entonces, ¿cómo funciona la asincronía en JavasScript?

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

De hecho, además de la pila, los navegadores tienen una cola especial para trabajar con la llamada WebAPI. Las funciones de esta cola se ejecutarán en orden solo después de que la pila se borre por completo. Solo después de eso, se colocan de la cola en la pila para su ejecución. Si hay al menos un elemento en la pila en este momento, entonces no pueden entrar en la pila. Solo por esto, llamar a las funciones por tiempo de espera a menudo es inexacto en el tiempo, ya que la función no puede pasar de la cola a la pila mientras está llena.

Echemos un vistazo al siguiente ejemplo y analicémoslo paso a paso. Veamos también qué sucede en el sistema.

console.log('Hi');
setTimeout(function cb1() {
    console.log('cb1');
}, 5000);
console.log('Bye');

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

1) Hasta ahora no pasa nada. La consola del navegador está limpia, la pila de llamadas está vacía.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

2) Luego se agrega el comando console.log('Hola') a la pila de llamadas.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

3) Y se cumple

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

4) Luego, console.log('Hola') se elimina de la pila de llamadas.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

5) Ahora pasemos al comando setTimeout(function cb1() {… }). Se agrega a la pila de llamadas.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

6) Se ejecuta el comando setTimeout(function cb1() {… }). El navegador crea un temporizador que forma parte de la API web. Realizará una cuenta atrás.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

7) El comando setTimeout(function cb1() {… }) ha completado su trabajo y se elimina de la pila de llamadas.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

8) El comando console.log('Bye') se agrega a la pila de llamadas.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

9) Se ejecuta el comando console.log('Bye').

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

10) El comando console.log('Bye') se elimina de la pila de llamadas.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

11) Después de que hayan transcurrido al menos 5000 ms, el temporizador finaliza y coloca la devolución de llamada cb1 en la cola de devolución de llamada.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

12) El bucle de eventos toma la función cb1 de la cola de devolución de llamadas y la coloca en la pila de llamadas.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

13) La función cb1 se ejecuta y agrega console.log('cb1') a la pila de llamadas.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

14) Se ejecuta el comando console.log('cb1').

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

15) El comando console.log('cb1') se elimina de la pila de llamadas.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

16) La función cb1 se elimina de la pila de llamadas.

Veamos un ejemplo en dinámica:

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

Bueno, vimos cómo se implementa la asincronía en JavaScript. Ahora hablemos brevemente sobre la evolución del código asíncrono.

La evolución del código asíncrono.

a(function (resultsFromA) {
    b(resultsFromA, function (resultsFromB) {
        c(resultsFromB, function (resultsFromC) {
            d(resultsFromC, function (resultsFromD) {
                e(resultsFromD, function (resultsFromE) {
                    f(resultsFromE, function (resultsFromF) {
                        console.log(resultsFromF);
                    })
                })
            })
        })
    })
});

La programación asíncrona como la conocemos en JavaScript solo se puede hacer con funciones. Se pueden pasar como cualquier otra variable a otras funciones. Así es como nacieron las devoluciones de llamada. Y es fresco, divertido y ferviente, hasta convertirse en tristeza, melancolía y tristeza. ¿Por qué? Sí, es sencillo:

  • A medida que crece la complejidad del código, el proyecto se convierte rápidamente en múltiples bloques anidados oscuros: "infierno de devolución de llamada".
  • El manejo de errores puede pasarse por alto fácilmente.
  • No puede devolver expresiones con retorno.

Con la llegada de Promise, la situación ha mejorado un poco.

new Promise(function(resolve, reject) {
    setTimeout(() => resolve(1), 2000);

}).then((result) => {
    alert(result);
    return result + 2;

}).then((result) => {
    throw new Error('FAILED HERE');
    alert(result);
    return result + 2;

}).then((result) => {
    alert(result);
    return result + 2;

}).catch((e) => {
    console.log('error: ', e);
});

  • Aparecieron cadenas de promesas, que mejoraron la legibilidad del código.
  • Había un método separado de interceptación de errores.
  • Ejecución paralela con Promise.all agregado
  • Podemos resolver la asincronía anidada con async/await

Pero la promesa tiene sus limitaciones. Por ejemplo, una promesa, sin bailar con una pandereta, no se puede cancelar y, lo más importante, funciona con un valor.

Bueno, aquí nos acercamos sin problemas a la programación reactiva. ¿Cansado? Bueno, lo bueno es que puedes ir a preparar unas gaviotas, hacer una lluvia de ideas y volver a leer más. Y continuaré.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

Programación reactiva - un paradigma de programación centrado en los flujos de datos y la propagación de cambios. Echemos un vistazo más de cerca a lo que es un flujo de datos.

// Получаем ссылку на элемент
const input = ducument.querySelector('input');

const eventsArray = [];

// Пушим каждое событие в массив eventsArray
input.addEventListener('keyup',
    event => eventsArray.push(event)
);

Imaginemos que tenemos un campo de entrada. Creamos una matriz, y para cada tecla del evento de entrada, almacenaremos el evento en nuestra matriz. Al mismo tiempo, me gustaría señalar que nuestra matriz está ordenada por tiempo, es decir, el índice de eventos posteriores es mayor que el índice de eventos anteriores. Tal matriz es un modelo de flujo de datos simplificado, pero aún no es un flujo. Para que esta matriz se llame flujo de manera segura, debe poder informar de alguna manera a los suscriptores que han llegado nuevos datos. Así llegamos a la definición de flujo.

Flujo de datos

const { interval } = Rx;
const { take } = RxOperators;

interval(1000).pipe(
    take(4)
)

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

Flujo es una matriz de datos ordenados por tiempo que puede indicar que los datos han cambiado. Ahora imagine lo conveniente que se vuelve escribir código en el que necesita desencadenar varios eventos en diferentes partes del código para una acción. Simplemente nos suscribimos a la transmisión y nos avisará cuando se produzcan cambios. Y la biblioteca RxJs puede hacer esto.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

RxJS es una biblioteca para trabajar con programas asincrónicos y basados ​​en eventos usando secuencias observables. La biblioteca proporciona el tipo principal observable, varios tipos de ayudantes (Observadores, Programadores, Sujetos) y operadores para trabajar con eventos como con colecciones (mapear, filtrar, reducir, cada y similares de JavaScript Array).

Comprendamos los conceptos básicos de esta biblioteca.

Observable, observador, productor

Observable es el primer tipo base que veremos. Esta clase contiene la parte principal de la implementación de RxJs. Está asociado con un flujo observable, al que se puede suscribir mediante el método de suscripción.

Observable implementa un mecanismo auxiliar para crear actualizaciones, el llamado Observador. La fuente de valores para un observador se llama Productor. Puede ser una matriz, un iterador, un socket web, algún tipo de evento, etc. Entonces podemos decir que observable es un conductor entre Productor y Observador.

Observable maneja tres tipos de eventos de Observer:

  • siguiente - nuevos datos
  • error: un error si la secuencia terminó debido a una excepción. este evento también implica el final de la secuencia.
  • completo: una señal sobre el final de la secuencia. Esto significa que no habrá más datos nuevos.

Veamos una demostración:

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

Al principio procesaremos los valores 1, 2, 3, y luego de 1 seg. obtenemos 4 y terminamos nuestro hilo.

Pensando en voz alta

Y entonces me di cuenta de que era más interesante contarlo que escribirlo. 😀

Suscripción

Cuando nos suscribimos a una transmisión, creamos una nueva clase. suscripción, que nos da la opción de darnos de baja con el método darse de baja. También podemos agrupar suscripciones usando el método add. Bueno, es lógico que podamos desagrupar hilos usando remove. Los métodos de agregar y eliminar aceptan una suscripción diferente como entrada. Me gustaría señalar que cuando cancelamos la suscripción, cancelamos la suscripción de todas las suscripciones de niños como si también llamaran al método de cancelación de suscripción. Adelante.

Tipos de arroyos

HOT
FRÍO

El productor se crea fuera de lo observable.
El productor se crea dentro del observable.

Los datos se pasan en el momento en que se crea el observable.
Los datos se proporcionan en el momento de la suscripción.

Necesita más lógica para darse de baja
El hilo termina solo

Utiliza una relación de uno a muchos
Utiliza una relación uno a uno

Todas las suscripciones tienen el mismo valor.
Las suscripciones son independientes.

Los datos se pueden perder si no hay suscripción
Vuelve a emitir todos los valores de transmisión para una nueva suscripción

Para dar una analogía, me imagino una corriente caliente como una película en un cine. En qué momento viniste, desde ese momento comenzaste a mirar. Compararía una corriente fría con una llamada en esos. apoyo. Cualquier persona que llama escucha la grabación del contestador automático de principio a fin, pero puede colgar con darse de baja.

Me gustaría señalar que también existen las llamadas corrientes cálidas (he encontrado esa definición muy raramente y solo en comunidades extranjeras): esta es una corriente que se transforma de una corriente fría a una caliente. Surge la pregunta: dónde usar)) Daré un ejemplo de la práctica.

Estoy trabajando con Angular. Utiliza activamente rxjs. Para obtener datos en el servidor, espero una transmisión en frío y uso esta transmisión en la plantilla usando asyncPipe. Si uso esta canalización varias veces, entonces, volviendo a la definición de flujo frío, cada canalización solicitará datos del servidor, lo cual es extraño por decir lo menos. Y si convierto una transmisión fría en una cálida, la solicitud se realizará una vez.

En general, comprender el tipo de flujos es bastante difícil para los principiantes, pero importante.

telecomunicaciones

return this.http.get(`${environment.apiUrl}/${this.apiUrl}/trade_companies`)
    .pipe(
        tap(({ data }: TradeCompanyList) => this.companies$$.next(cloneDeep(data))),
        map(({ data }: TradeCompanyList) => data)
    );

Los operadores nos brindan la oportunidad de trabajar con flujos. Ayudan a controlar los eventos que fluyen en el Observable. Consideraremos algunos de los más populares, y se puede encontrar más información sobre los operadores en los enlaces de información útil.

Operadores de

Comencemos con el operador auxiliar de. Crea un Observable basado en un valor simple.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

Operadores-filtro

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

El operador de filtro, como sugiere su nombre, filtra la señal de flujo. Si el operador devuelve verdadero, salta más.

Operadores - tomar

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

take: toma el valor del número de emisiones, después de lo cual finaliza la transmisión.

Operadores-debounceTime

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

debounceTime: descarta los valores emitidos que se encuentran dentro del intervalo de tiempo especificado entre los datos de salida; después de que haya transcurrido el intervalo de tiempo, emite el último valor.

const { Observable } = Rx;
const { debounceTime, take } = RxOperators;

Observable.create((observer) => {
  let i = 1;
  observer.next(i++);
  // Испускаем значение раз в 1000мс
  setInterval(() => {
    observer.next(i++)
  }, 1000);

 // Испускаем значение раз в 1500мс
  setInterval(() => {
    observer.next(i++)
  }, 1500);
}).pipe(
  debounceTime(700),  // Ожидаем 700мс значения прежде чем обработать
  take(3)
);  

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

Operadores-takeWhile

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

Emite valores hasta que takeWhile devuelve falso, luego cancela la suscripción del hilo.

const { Observable } = Rx;
const { debounceTime, takeWhile } = RxOperators;

Observable.create((observer) => {
  let i = 1;
  observer.next(i++);
  // Испускаем значение раз в 1000мс
  setInterval(() => {
    observer.next(i++)
  }, 1000);
}).pipe(
  takeWhile( producer =>  producer < 5 )
);  

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

Operadores-combinarÚltimo

El operador combinado combineLatest es algo similar a promise.all. Combina múltiples flujos en uno. Después de que cada subproceso haya realizado al menos una emisión, obtenemos los valores más recientes de cada uno como una matriz. Además, después de cualquier emisión de los flujos combinados, dará nuevos valores.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

const { combineLatest, Observable } = Rx;
const { take } = RxOperators;

const observer_1 = Observable.create((observer) => {
  let i = 1;
  // Испускаем значение раз в 1000мс
  setInterval(() => {
    observer.next('a: ' + i++);
  }, 1000);
});

const observer_2 = Observable.create((observer) => {
  let i = 1;
  // Испускаем значение раз в 750мс
  setInterval(() => {
    observer.next('b: ' + i++);
  }, 750);
});

combineLatest(observer_1, observer_2).pipe(take(5));

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

Operadores-zip

Zip: espera un valor de cada flujo y forma una matriz basada en estos valores. Si el valor no proviene de ningún hilo, entonces el grupo no se formará.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

const { zip, Observable } = Rx;
const { take } = RxOperators;

const observer_1 = Observable.create((observer) => {
  let i = 1;
  // Испускаем значение раз в 1000мс
  setInterval(() => {
    observer.next('a: ' + i++);
  }, 1000);
});

const observer_2 = Observable.create((observer) => {
  let i = 1;
  // Испускаем значение раз в 750
  setInterval(() => {
    observer.next('b: ' + i++);
  }, 750);
});

const observer_3 = Observable.create((observer) => {
  let i = 1;
  // Испускаем значение раз в 500
  setInterval(() => {
    observer.next('c: ' + i++);
  }, 500);
});

zip(observer_1, observer_2, observer_3).pipe(take(5));

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

Operadores - forkÚnete

forkJoin también une subprocesos, pero solo emite un valor cuando todos los subprocesos están completos.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

const { forkJoin, Observable } = Rx;
const { take } = RxOperators;

const observer_1 = Observable.create((observer) => {
  let i = 1;
  // Испускаем значение раз в 1000мс
  setInterval(() => {
    observer.next('a: ' + i++);
  }, 1000);
}).pipe(take(3));

const observer_2 = Observable.create((observer) => {
  let i = 1;
  // Испускаем значение раз в 750
  setInterval(() => {
    observer.next('b: ' + i++);
  }, 750);
}).pipe(take(5));

const observer_3 = Observable.create((observer) => {
  let i = 1;
  // Испускаем значение раз в 500
  setInterval(() => {
    observer.next('c: ' + i++);
  }, 500);
}).pipe(take(4));

forkJoin(observer_1, observer_2, observer_3);

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

Operadores-mapa

El operador de transformación del mapa transforma el valor de emisión en uno nuevo.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

const {  Observable } = Rx;
const { take, map } = RxOperators;

Observable.create((observer) => {
  let i = 1;
  // Испускаем значение раз в 1000мс
  setInterval(() => {
    observer.next(i++);
  }, 1000);
}).pipe(
  map(x => x * 10),
  take(3)
);

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

Operadores: compartir, tocar

El operador de toque le permite realizar efectos secundarios, es decir, cualquier acción que no afecte la secuencia.

El operador de servicios compartidos puede convertir un flujo frío en uno caliente.

Programación asíncrona en JavaScript (Callback, Promise, RxJs)

Los operadores están listos. Pasemos al tema.

Pensando en voz alta

Y luego fui a tomar té. Estoy cansado de estos ejemplos 😀

familia de sujetos

La familia de temas es un excelente ejemplo de hilos calientes. Estas clases son una especie de híbridos que actúan como observables y observadores al mismo tiempo. Dado que el tema es una transmisión activa, se debe cancelar la suscripción. Si hablamos de los métodos principales, estos son:

  • siguiente: pasar nuevos datos a la transmisión
  • error - error y terminación de subproceso
  • completo - fin del hilo
  • suscribirse - suscribirse a una transmisión
  • darse de baja - darse de baja de la transmisión
  • asObservable - transformar en un observador
  • toPromise - se transforma en una promesa

Asignar 4 5 tipos de sujetos.

Pensando en voz alta

Dije 4 en la transmisión, pero resultó que agregaron uno más. Como dice el dicho, vive y aprende.

Asunto sencillo new Subject()- el tipo más simple de temas. Creado sin parámetros. Pasa los valores que vinieron solo después de la suscripción.

Comportamiento Sujeto new BehaviorSubject( defaultData<T> ) - en mi opinión, el tipo más común de sujeto-s. La entrada toma el valor predeterminado. Guarda siempre los datos del último número, que se transmiten al suscribirse. Esta clase también tiene un método de valor útil que devuelve el valor actual de la secuencia.

Reproducir Asunto new ReplaySubject(bufferSize?: number, windowTime?: number) - Opcionalmente, puede tomar como primer argumento el tamaño del búfer de valores que almacenará en sí mismo, y el segundo tiempo durante el cual necesitamos cambios.

asincrónico new AsyncSubject() - No sucede nada al suscribirse, y el valor se devolverá solo cuando se complete. Solo se devolverá el último valor de la secuencia.

WebSocketAsunto new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) - La documentación guarda silencio al respecto y yo mismo lo veo por primera vez. Quién sabe lo que hace, escriba, agregaremos.

Uf. Bueno, hemos considerado todo lo que quería contar hoy. Espero que esta información sea de ayuda. Puede leer la lista de literatura por su cuenta en la pestaña Información útil.

información útil

Fuente: habr.com

Añadir un comentario