Hasta la fecha, se han escrito muchos artículos en los que debe darse de baja de las suscripciones Observable RxJS, de lo contrario se producirá una pérdida de memoria . Para la mayoría de los lectores de dichos artículos, la regla firme "¿firmada? ¡Firmar!" Se estableció en mi cabeza. Pero, desafortunadamente, a menudo en dichos artículos la información se distorsiona o algo no se negocia, y peor aún cuando se reemplazan los conceptos. Hablaremos de esto.

Tome por ejemplo este artículo: https://medium.com/ngx/why-do-you-need-unsubscribe-ee0c62b5d21f

Cuando me dicen acerca de la "oportunidad potencial para obtener una regresión en el rendimiento", inmediatamente pienso en la optimización prematura .


Continuamos leyendo el artículo del hombre bajo el apodo de Reactive Fox :


Además, hay información útil y consejos. Estoy de acuerdo en que siempre debe darse de baja de un sinfín de transmisiones en RxJS . Pero me concentro solo en información dañina (en mi opinión).

Wow ... atrapado con el horror. Tal intimidación sin fundamento (sin métricas, números ...) en la actualidad ha llevado al hecho de que para un gran número de front-end, la falta de baja es como un trapo rojo para un toro. Cuando se topan con esto, ya no ven nada más que este trapo.

El autor del artículo incluso hizo una aplicación de demostración, donde trató de probar sus pensamientos:
https://stackblitz.com/edit/why-you-have-to-unsubscribe-from-observable-material
De hecho, en su soporte se puede ver cómo el procesador hace un trabajo innecesario (cuando no hago clic en nada) y cómo aumenta el consumo de memoria (pequeño cambio):

Como confirmación del hecho de que siempre debe darse de baja de las suscripciones de solicitudes Observable HttpClient , agregó un interceptor de solicitudes que muestra "aún vivo ... todavía vivo ... todavía vivo ..." en la consola:

Es decir la persona interceptó el flujo final, lo hizo infinito (en caso de error, la solicitud se repite, pero el error siempre ocurre) y da esto como evidencia de que necesita darse de baja de los últimos.
StackBlitz no es muy adecuado para medir el rendimiento de la aplicación, ya que Hay una sincronización automática durante la actualización y requiere recursos. Entonces hice mi solicitud de prueba: https://github.com/andchir/test-angular-app
Hay dos ventanas allí. Cuando abre cada uno, se envía una solicitud a action.php , en el que hay un retraso de 3 segundos como una imitación de una operación muy intensiva en recursos. También action.php registra todas las solicitudes en el archivo log.txt .
Código Action.php<?php header('Content-Type: application/json'); function logging($str, $fileName = 'log.txt') { if (is_array($str)) { $str = json_encode($str); } $rootPath = __DIR__; $logFilePath = $rootPath . DIRECTORY_SEPARATOR . $fileName; $options = [ 'max_log_size' => 200 * 1024 ]; if (!is_dir(dirname($logFilePath))) { mkdir(dirname($logFilePath)); } if (file_exists($logFilePath) && filesize($logFilePath) >= $options['max_log_size']) { unlink($logFilePath); } $fp = fopen( $logFilePath, 'a' ); $dateFormat = 'd/m/YH:i:s'; $str = PHP_EOL . PHP_EOL . date($dateFormat) . PHP_EOL . $str; fwrite( $fp, $str ); fclose( $fp ); return true; } $actionName = isset($_GET['a']) && !is_array($_GET['a']) ? $_GET['a'] : '1'; logging("STARTED-{$actionName}"); sleep(3);
Pero primero, una pequeña digresión. En la imagen a continuación (se puede hacer clic), puede ver un ejemplo simple de cómo funciona el recolector de basura JavaScript en el navegador Chrome. PUSH ocurrió, pero setTimeout no impidió que el recolector de basura borrara la memoria.

No olvides llamar al recolector de basura con solo tocar un botón cuando experimentes.

Volvamos a mi aplicación de prueba. Aquí está el código para ambas ventanas:
Código de componente BadModal @Component({ selector: 'app-bad-modal', templateUrl: './bad-modal.component.html', styleUrls: ['./bad-modal.component.css'], providers: [HttpClient] }) export class BadModalComponent implements OnInit, OnDestroy { loading = false; largeData: number[] = (new Array(1000000)).fill(1); destroyed$ = new Subject<void>(); data: DataInterface; constructor( private http: HttpClient, private bsModalRef: BsModalRef ) { } ngOnInit() { this.loadData(); } loadData(): void { // For example only, not for production. this.loading = true; const subscription = this.http.get<DataInterface>('/action.php?a=2').pipe( takeUntil(this.destroyed$), catchError((err) => throwError(err.message)), finalize(() => console.log('FINALIZE')) ) .subscribe({ next: (res) => { setTimeout(() => { console.log(subscription.closed ? 'SUBSCRIPTION IS CLOSED' : 'SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('LOADED'); this.data = res; this.loading = false; }, error: (error) => { setTimeout(() => { console.log(subscription.closed ? 'ERROR - SUBSCRIPTION IS CLOSED' : 'ERROR - SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('ERROR', error); }, complete: () => { setTimeout(() => { console.log(subscription.closed ? 'COMPLETED - SUBSCRIPTION IS CLOSED' : 'COMPLETED - SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('COMPLETED'); } }); } close(event?: MouseEvent): void { if (event) { event.preventDefault(); } this.bsModalRef.hide(); } ngOnDestroy() { console.log('DESTROY'); this.destroyed$.next(); this.destroyed$.complete(); } }
Como puede ver, hay una cancelación de suscripción (takeUntil). Todo como el "maestro" nos aconsejó. También hay una gran variedad.
Código de componente de GoodModal @Component({ selector: 'app-good-modal', templateUrl: './good-modal.component.html', styleUrls: ['./good-modal.component.css'] }) export class GoodModalComponent implements OnInit, OnDestroy { loading = false; largeData: number[] = (new Array(1000000)).fill(1); data: DataInterface; constructor( private http: HttpClient, private bsModalRef: BsModalRef ) { } ngOnInit() { this.loadData(); } loadData(): void { // For example only, not for production. this.loading = true; const subscription = this.http.get<DataInterface>('/action.php?a=1').pipe( catchError((err) => throwError(err.message)), finalize(() => console.log('FINALIZE')) ) .subscribe({ next: (res) => { setTimeout(() => { console.log(subscription.closed ? 'SUBSCRIPTION IS CLOSED' : 'SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('LOADED'); this.data = res; this.loading = false; }, error: (error) => { setTimeout(() => { console.log(subscription.closed ? 'ERROR - SUBSCRIPTION IS CLOSED' : 'ERROR - SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('ERROR', error); }, complete: () => { setTimeout(() => { console.log(subscription.closed ? 'COMPLETED - SUBSCRIPTION IS CLOSED' : 'COMPLETED - SUBSCRIPTION IS NOT CLOSED!'); }, 0); console.log('COMPLETED'); } }); } close(event?: MouseEvent): void { if (event) { event.preventDefault(); } this.bsModalRef.hide(); } ngOnDestroy() { console.log('DESTROY'); } }
Hay exactamente la misma propiedad con una gran matriz, pero no hay cancelación de suscripción. Y esto no me impide llamar a esta ventana una buena. ¿Por qué más tarde?
Mira el video:
Como puede ver, en ambos casos, después de cambiar al segundo componente, el recolector de basura devolvió con éxito la memoria a los valores normales. Sí, uno podría hacer posible borrar la memoria también después de cerrar las ventanas, pero en nuestro experimento esto no es importante. Resulta que el "maestro" estaba equivocado cuando dijo:
Por ejemplo, realizó una solicitud, pero cuando la respuesta aún no ha llegado del backend, destruirá el componente como innecesario, luego su suscripción mantendrá el enlace al componente , creando así una posible pérdida de memoria.
Sí, él está hablando de una fuga "potencial" . Pero, si el flujo es finito, no habrá pérdida de memoria.
Preveo las exclamaciones indignadas de tales "maestros". Definitivamente nos dirán algo como: "ok, no hay pérdida de memoria, pero al cancelar la suscripción también cancelamos la solicitud , lo que significa que nos aseguraremos de que no se ejecutará ningún código después de recibir una respuesta del servidor" . En primer lugar, no digo que cancelar la suscripción siempre sea malo, solo digo que estás reemplazando conceptos . Sí, el hecho de que después de que llegue la respuesta se realice alguna otra operación inútil es malo, pero solo puede protegerse de una pérdida de memoria real al darse de baja (en este caso), y puede protegerse de otros efectos indeseables de otras maneras . No es necesario intimidar a los lectores e imponerles su propio estilo de escribir código.
¿Siempre tenemos que cancelar la solicitud si el usuario cambia de opinión? No siempre! No olvide que cancela la solicitud, pero no cancela la operación en el servidor . Imagine que un usuario ha abierto un componente, algo se ha estado cargando durante mucho tiempo y cambia a otro componente. Es posible que el servidor esté cargado y no responda a todas las solicitudes y operaciones. En este caso, el usuario puede meter frenéticamente todos los enlaces en la navegación y crear una carga aún mayor en el servidor , porque la solicitud no se detiene en el lado del servidor (en la mayoría de los casos).
Mira el siguiente video:
Hice que el usuario esperara una respuesta. En la mayoría de los casos, la respuesta llegará rápidamente y el usuario no experimentará ningún inconveniente. Pero de esta manera, salvaremos al servidor de realizar operaciones pesadas repetidas, si las hay.
Resumen:
- No estoy diciendo que no necesite darse de baja de las suscripciones RxJS de solicitudes HttpClient. Solo digo que hay momentos en que esto no es necesario. No es necesario reemplazar conceptos. Si está hablando de una pérdida de memoria, muéstrela. No es su interminable console.log , es decir, una fuga. ¿En qué se mide la memoria? ¿Cuál es el tiempo de operación medido? Esto es lo que hay que mostrar.
- No llamo a mi solución, que apliqué en la solicitud de prueba, una "bala de plata". Por el contrario, insto al fronttender a que se le dé más libertad. Déjelo decidir cómo escribir su código. No es necesario intimidarlo e imponer su propio estilo de desarrollo.
- Estoy en contra del fanatismo y la optimización prematura. He visto demasiado de esto últimamente.
- El navegador tiene métodos más avanzados para encontrar pérdidas de memoria que el que mostré. Creo que en mi caso la aplicación de este método simple es suficiente. Pero le recomiendo que se familiarice con el tema con más detalle, por ejemplo, en este artículo: https://habr.com/en/post/309318/ .
UPD # 1
Por el momento, la publicación se hundió durante casi un día. Al principio fue a los pros y los contras, luego la evaluación se detuvo en cero. Esto significa que la audiencia se dividió exactamente en dos campos. No sé si esto es bueno o malo.
UPD # 2
En los comentarios, apareció Jet Fox (el autor del artículo que se analiza). Al principio me dio las gracias, fue muy educado. Pero, viendo la pasividad de la audiencia, comenzó a presionar. Llegó al punto en que escribió que debería disculparme. Es decir mintió (mentiras esbozadas con un marco amarillo arriba), y debería disculparme.
Al principio pensé que el interceptor de transmisiones con repeticiones interminables (bueno, 2-3 repeticiones), que escribió en su aplicación de demostración, es solo para pruebas e información. Pero resultó que lo consideraba un ejemplo de la vida. Es decir bloquear el botón de una ventana, es imposible . Y para crear tales interceptores, violando los principios de SOLID, violando la modularidad de la aplicación (los módulos y componentes deben ser independientes entre sí), permitiendo que las pruebas unitarias de sus unidades (componentes, servicios) atraviesen el bosque, puede hacerlo. Imagina la situación: escribiste un componente, escribiste pruebas unitarias para él. Y luego aparece un Fox así, agrega un interceptor similar a su aplicación, y sus pruebas se vuelven inútiles. Entonces aún te dice: "¿Por qué no predijiste que podría querer agregar un interceptor? Bueno, corrige tu código". Quizás esto sea una realidad en su equipo, pero no creo que esto deba alentarse o hacerse la vista gorda.
UPD # 3
Los comentarios tratan principalmente de suscripciones y bajas. ¿Es una publicación llamada "Unsubscribing Evil"? No No te insto a que no te des de baja. Haz lo que hiciste antes. Pero debes entender por qué estás haciendo esto. Darse de baja no es una optimización prematura. Pero, pisando el camino de la protección contra posibles amenazas (como nos llama el autor del artículo analizado), puede cruzar la línea. Entonces su código puede sobrecargarse y ser difícil de mantener.
Este artículo trata sobre el fanatismo, que conduce a la distribución de información no verificada. En algunos casos, es necesario relacionarse con la ausencia de una cancelación de suscripción con más calma (debe comprender claramente si existe un problema en un caso particular).
UPD # 4
Por el contrario, insto al fronttender a que se le dé más libertad. Déjelo decidir cómo escribir su código.
Aquí necesitas aclarar. Estoy para los estándares. Pero el estándar puede ser establecido por el autor de la biblioteca o su equipo, mientras que esto no es así (en la documentación y oficialmente). Por ejemplo, la documentación del marco de Symfony tiene una sección de Mejores prácticas . Si fuera lo mismo en la documentación de RxJS y dijera "suscripción firmada", no tendría el deseo de discutir con él.
UPD # 5
Comentario importante con respuestas de personas de buena reputación:
https://habr.com/en/post/479732/#comment_21012620
La recomendación para cumplir con el contrato de "suscripción firmada" del desarrollador de RxJS existe , pero de manera no oficial.