Hoy me gustaría hablar sobre detección de cambios personalizada en Angular. Como desarrolladores, nos esforzamos en construir aplicaciones que sean concisas e interactivas. Y eso es todo lo que recogen las aplicaciones reactivas.

 

Detección de cambios personalizada en Angular


Podemos visualizar una aplicación Angular como un árbol de componentes que proviene de un componente raíz. Creamos cada componente y los organizamos para formar nuestra aplicación. La información fluye de arriba a abajo en nuestro árbol de componentes. A medida que nuestro usuario interactúa con nuestra app, su estado cambia. El estado representa cualquier información o data que la aplicación registra.

¿Qué acciona los cambios en el estado de los componentes? Eventos de los usuarios (eg, realizar un click), timers (setTimeout o setInterval), eventos asincrónicos de la API como request de XHR y HTTP y eventos que se basan en promesas. Para asegurarnos de que la interfaz del usuario siempre refleje el estado del programa, necesitamos un mecanismo que detecte cambios de datos en un componente, y luego volver a renderizar la vista de forma acorde. Aquí es cuando la Detección de cambios personalizada en Angular
cobra importancia. Fue construido por Victor Savkin – fue miembro del equipo de Angular de Google. En sus palabras, una aplicación en Angular es un sistema reactivo, con la detección de cambios en su epicentro.

Todos los componentes tienen un detector de cambios que comprueba los data bindings del template. ¿Cómo funciona el data binding en Angular? Los data binding hacen que sea fácil mostrar el tipo de propiedades de nuestro componente y ajustar la propiedad de los elementos DOM a la clase de nuestro valor propiedad. Además, el componente puede escuchar eventos desde el DOM y responder ante ellos como una experiencia de usuario interactiva. Hay 4 tipos básicos de enlazado de datos en Angular:

TemplateTipo de bindingComponente
{{pageTitle}}
interpolaciónpagetTitle: string = ‘Product list’;
<img [style.margin.px]=’imageMargin’ />property bindingimageMargin: number = 2;
(click)=’toggleImage()’>Show Image</buttonevent bindingtoggleImage(): void { this.showImage = ! this.showImage}
<input type=’text’ [(ngModel)]=’name’two-way bindingname: string;

La estrategia por defecto de Detección de cambios personalizada en Angular empieza en la cima con los componentes de raíz y sigue su camino hacia abajo a través del árbol de componentes, comprobando cada elemento incluso si éste no ha cambiado. Compara el valor actual de la propiedad utilizada en la expresión del template con el valor anterior de esa propiedad.

En JavaScript, los tipos primitivos son string, number, boolean, undefined, null (and Symbol en ES6) y pasan todos por valor.


let x = 1
let y = x 

x = 2 

console.log(x, y); // -> 2,1

Cualquier cambio en las propiedades de los tipo de los tipos primitivos hará que Angular active el detector de cambios y actualice el DOM.

En JS, hay tres tipos de data que pasan por una referencia: array, function y object. Es decir, que si asignamos un objeto a una variable, la variable apunta hacia la ubicación del objeto en la memoria.


let x = { value: 1 };
let y = x;

x.value = 2;

console.log(x, y); // -> { value: 2 }, { value: 2 };

Los objetos y los arrays son mutables, es decir, se pueden cambiar. Un objeto mutable es un objeto cuyo estado puede modificarse después de crearse. Por otra parte, los strings y numbers son inmutables y su estado no puede modificarse una vez son creados.

Echemos un vistazo al siguiente componente sidenav:

@Component({
    selector: 'sidenav',
    templateUrl: './sidenav.component.html',
    styleUrls: ['./sidenav.component.scss'],
})

export class SidenavComponent {
    @Input() project: IProject;
}

export interface IProject {
  id: string;
  name: string;
  keywords?: string[];
}

Este componente depende de un objeto “project” (Input property). Pero ese objeto es mutable y puede ser cambiado por otro componente o servicio. Esta es la razón por la que Angular, por defecto, recorre todo el árbol y comprueba el componente sidenav cada vez que detecta un cambio. Sin embargo, esto puede provocar un problema en el rendimiento ya que el framework ha de hacer lo mismo por cada elemento en el árbol de componentes. En resumen, si trabajamos con información mutable, rastrear los cambios puede ser difícil y provocar una bajada del rendimiento en aplicaciones de gran escala.

 

Personalizando a detección de cambios en Angular

Asumiendo que utilizamos objetos inmutables y que la arquitectura de nuestra aplicación se basa en observables, podemos optimizar el rendimiento de nuestra vista ajustando la estrategia de detección de cambios personalizada en Angular a OnPush en ciertos componentes.

 

Inmutables y OnPush

Utilizando una estrategia OnPush con objetos inmutables le decimos a Angular que un componente y su subárbol dependen únicamente de los inputs y tan solo ha de comprobar si recibe una nueva referencia de input en vez de una mutación, o si el componente o su child provocan un evento DOM.

Para implementar la estrategia simplemente tenemos que añadir:


changeDetection: ChangeDetectionStrategy.OnPush

en el decorator del componente
Aquí tenemos dos componentes: product-list (parent) y los product-detail (child).

 

product-list.component.ts

import { Component, ChangeDetectionStrategy } from ‘@angular/core’;

@Component({
   selector: ‘product-list’,
   template: `
     <product-detail *ngFor="let product of products" [product]="product">
     </product-detail>

     <button (click)=‘changeDescription()’>Change Product Description</button>
   `
})

export class ProductListComponent {

products: Array<any> = [
 {
   "productId": 1,
   "productName": "Whiteboard Sit-Stand Desk",
   "description": "Get to stand when working and save your back.",
   "price": 495,
   "starRating": 4.7,
 },
 {
   "productId": 2,
   "productName": "Logitech, MX MASTER 2S",
   "description": "Great grit and the ability to be set up to 3 devices via Bluetooth.",
   "price": 25,
   "starRating": 4.4,
 },
 {
   "productId": 5,
   "productName": "Jump Rope",
   "description": "Perfect for a tech health enthusiast.",
   "price": 79,
   "starRating": 4.4,
 }
];

 changeDescription() {
   this.products[2].description = ‘Perfect for breaks’;
 };
}

 

product-detail.component.ts

import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
@Component({
    selector: 'product-detail',
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: `
      <ul>
        <li>{{product.productName}}</li>
        <p>{{product.description}}</p>
      </ul>
    `
})

export class ProductDetailComponent {
  @Input() product;
}

Si marcamos el componente product-detail con OnPush (ver más abajo), y no le damos a Angular ninguna referencia de objeto pero seguimos cambiando el que ya existe, el detector de cambios OnPush no se activará. Como resultado, la plantilla no se actualizará.


@Component({
    selector: 'product-detail',
    templateUrl: './product-detail.component.html',
    changeDetection: ChangeDetectionStrategy.OnPush,
  })

Al pasar una nueva referencia de objeto, el detector de cambios se activará y el DOM se actualizará como se esperaba.


this.products[2] = {
      ...this.products[2], 
      description: 'Perfect for breaks'
}

 

Observables y OnPush

Si la propiedad input es un observable, su componente puede cambiarse si y solo si una de sus propiedades input emite un evento.

En este ejemplo, el componente product-list tiene una propiedad input llamada products que es un Observable. El componente está marcado con OnPush para que la estrategia de detector de cambio no se active automáticamente, sino únicamente si el numero es múltiplo de 5 o cuando el observable está completo. Hemos importado e introducido el servicio ChangeDetectorRef. En la iniciación del componente (en el gancho del ciclo de vida de ngOnInit), nos subscribimos al observable para un nuevo valor. El método de subscripción tiene 3 callbacks como argumento: onNext, onError y onCompleted. OnNext imprimirá el valor que tengamos y luego incrementará el contador. Si el valor es múltiple de 5, llamamos manualmente el método markForCheck del detector de cambios. Aquí le diremos a Angular que hemos hecho un cambio, para que el detector de cambios se ejecute. Después asignamos null como callback de onError, indicando que no queremos encargarnos de este escenario. Finalmente, activamos el detector de cambios para el callback onComplete, para que se visualice dl contador final.

 

product-list.component.ts


import { Component, OnInit, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Observable } from 'rxjs/Rx';

@Component({
    selector: 'product-list',
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: `
      <div>
        <div>Total products: {{counter}}</div>
      </div>
    `
  })
  
export class ProductListComponent {
    @Input() products$: Observable<number>;
    counter = 0;

constructor(private changeDetector: ChangeDetectorRef) {}

ngOnInit() {
  this.products$.subscribe((value) => {
      console.log('printing the value', value);
      this.counter++;
      if (this.counter % 5 === 0) {
        this.changeDetector.markForCheck();
      }
  },
  null,
  () => {
    this.changeDetector.markForCheck();
  });
  }
  
}

En el componente de la app creamos el Observable que pasamos al componente product-list como input. Pasamos dos parámetros al método timer: el primero es la cantidad de milisegundos que queremos esperar antes de producir el primer valor. Y el segundo es la cantidad de milisegundos que esperamos entre valores. Por lo que este Observable generará valores secuenciales cada 100 valores, hasta el infinito. Ya que no queremos que eso ocurra, utilizamos el método take para coger únicamente los primeros 101 valores.

 

app.component.ts


import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Rx';

@Component({
    selector: 'app-root',
    template: `
      <product-list
        [products]="productObservable">
      </product-list>
    `
    })
  
export class AppComponent implements OnInit {
  productObservable$: Observable<number>;

  ngOnInit() {
    this.productObservable$ = Observable.timer(100, 100).take(101);
  }

}

El código fuente de los observables viene de ng-book, the complete book on Angular 5.

Cuando ejecutamos este código, veremos que el contador solo se actualizará cada 5 valores que obtenga del observable y un valor final de 101 cuando el observable se complete.


Si tu aplicación de Angular trabaja con objetos o observables inmutables, puedes sacar ventaja de ellos y personalizar tu estrategia de detección de cambios. OnPush funcionará predeciblemente con todo tipo de diseño de componentes, componentes que reciben información directa como inputs o que tienen un input observable, etc. Será de gran ayuda en una aplicación compleja con un gran número de componentes con cientos de expresiones potenciales para comprobar.

 

Si te interesa descubrir más cosas sobre detección de cambios personalizada en Angular o sobre desarrollo de software en general, suscríbete a nuestra newsletter 

Si este artículo sobre detección de cambios personalizada en Angular te gustó, te puede interesar: 

Simular respuestas del servidor con Nodejs

CSS Grid solución a los problems de float y flexbox

Modulos Webpack

Principio de responsabilidad única

Por qué Kotlin ?

Patrón MVP en iOS

Arquitectura de microservicios 

F-bound en Scala: traits genéricos con higher-kinded types

Scala Generics I : Clases genéricas y Type bounds

Scala Generics II: covarianza y contravarianza 

Scala Generics III: generalized type constraints