Si me gusta y quiere tener una comprensión completa del mecanismo de detección de cambios en Angular, tendrá que explorar las fuentes ya que no hay mucha información disponible en la web. La mayoría de los artículos mencionan que cada componente tiene su propio detector de cambios que es responsable de verificar el componente, pero no van más allá y se enfocan principalmente en casos de uso para inmutables y estrategia de detección de cambios. Este artículo le brinda la información necesaria para comprender por qué funcionan los casos de uso con inmutables y cómo la estrategia de detección de cambios afecta la verificación. Además, lo que aprenderá de este artículo le permitirá idear varios escenarios para la optimización del rendimiento por su cuenta.
Este artículo consta de dos partes. La primera parte es bastante técnica y contiene muchos enlaces a las fuentes. Explica en detalle cómo funciona el mecanismo de detección de cambios debajo del capó. Su contenido se basa en la última versión de Angular: 4.0.1. La forma en que se implementa el mecanismo de detección de cambios bajo el capó en esta versión es diferente de la anterior 2.4.1. Si está interesado, puede leer un poco sobre cómo funcionó en esta respuesta de stackoverflow .
La segunda parte muestra cómo se puede usar la detección de cambios en la aplicación y su contenido se aplica tanto a la versión 2.4.1 anterior como a la versión 4.0.1 más reciente de Angular, ya que la API pública no ha cambiado.
Se ha mencionado en los tutoriales que una aplicación Angular es un árbol de componentes. Sin embargo, debajo del capó, angular usa una abstracción de bajo nivel llamada view . Existe una relación directa entre una vista y un componente: una vista está asociada con un componente y viceversa. Una vista contiene una referencia a la instancia de la clase de componente asociada en la propiedad del componente. Todas las operaciones, como las comprobaciones de propiedades y las actualizaciones de DOM, se realizan en las vistas, por lo que es técnicamente más correcto afirmar que angular es un árbol de vistas, mientras que un componente puede describirse como un concepto de vista de alto nivel. Esto es lo que puede leer sobre la vista en las fuentes :
Una vista es un componente fundamental de la interfaz de usuario de la aplicación. Es la agrupación más pequeña de Elementos que se crean y destruyen juntos.
Las propiedades de los elementos de una Vista pueden cambiar, pero la estructura (número y orden) de los elementos de una Vista no. Solo se puede cambiar la estructura de los Elementos insertando, moviendo o eliminando Vistas anidadas a través de ViewContainerRef. Cada Vista puede contener muchos Contenedores de Vista.
En este artículo usaré las nociones de vista de componente y componente de manera intercambiable.
Hay dos cosas importantes acerca de una vista de componente en el contexto de la detección de cambios. Cada vista tiene un enlace a sus vistas secundarias a través de la propiedad de nodos y, por lo tanto, puede realizar acciones en las vistas secundarias. Y cada vista tiene un estado , que juega un papel muy importante porque, en función de su valor, Angular decide si ejecutar la detección de cambios para la vista y todos sus elementos secundarios u omitirla. Hay cuatro estados posibles :
La detección de cambios se omite para la vista y sus vistas secundarias si ChecksEnabled es falso o si la vista está en estado Error o Destroyed. De forma predeterminada, todas las vistas se inicializan con ChecksEnabled a menos que se use ChangeDetectionStrategy.OnPush. Más sobre eso más adelante. Los estados se pueden combinar, por ejemplo, una vista puede tener establecidos los indicadores FirstCheck y ChecksEnabled.
Angular tiene un montón de conceptos de alto nivel para manipular las vistas. He escrito sobre algunos de ellos aquí . Uno de esos conceptos es ViewRef . Encapsula la vista del componente subyacente y tiene un método con el nombre adecuado detectChanges . Cuando se produce un evento asincrónico, Angular activa la detección de cambios en su ViewRef superior, que después de ejecutar la detección de cambios para sí mismo ejecuta la detección de cambios para sus vistas secundarias .
La lógica principal responsable de ejecutar la detección de cambios para una vista reside en la función checkAndUpdateView . La mayor parte de su funcionalidad realiza operaciones en vistas de componentes secundarios . Cuando se activa para una vista en particular, realiza las siguientes operaciones en el orden especificado:
Hay algunas cosas que destacar en función de las operaciones enumeradas anteriormente.
Lo primero es que el enlace del ciclo de vida onChanges se activa en un componente secundario antes de que se verifique la vista secundaria y se activará incluso si se omite la detección de cambios para la vista secundaria. Esta es información importante y veremos cómo podemos aprovechar este conocimiento en la segunda parte del artículo.
Lo segundo es que el DOM de una vista se actualiza como parte de un mecanismo de detección de cambios mientras se verifica la vista. Esto significa que si un componente no está marcado, el DOM no se actualiza incluso si las propiedades del componente utilizadas en una plantilla cambian.
Otra observación interesante es que el estado de la vista de un componente secundario se puede cambiar durante la detección de cambios. Mencioné anteriormente que todas las vistas de componentes se inicializan con ChecksEnabled de manera predeterminada, pero para todos los componentes que usan la estrategia OnPush, la detección de cambios se deshabilita después de la primera verificación (operación 9 en la lista):
if (view.def.flags & ViewFlags.OnPush) { view.state &= ~ViewState.ChecksEnabled; }
Significa que durante la siguiente ejecución de detección de cambios, se omitirá la verificación para esta vista de componente y todos sus elementos secundarios. La documentación sobre la estrategia OnPush establece que un componente se verificará solo si sus enlaces han cambiado. Entonces, para hacer eso, los controles deben habilitarse configurando el bit ChecksEnabled. Y esto es lo que hace el siguiente código (operación 2):
if (compView.def.flags & ViewFlags.OnPush) { compView.state |= ViewState.ChecksEnabled; }
El estado se actualiza solo si los enlaces de la vista principal cambiaron y la vista del componente secundario se inicializó con ChangeDetectionStrategy.OnPush.
Finalmente, la detección de cambios para la vista actual es responsable de iniciar la detección de cambios para las vistas secundarias (operación 8). Este es el lugar donde se verifica el estado de la vista del componente secundario y, si es ChecksEnabled, entonces para esta vista se realiza la detección de cambios. Aquí está el código relevante:
viewState = view.state; ... case ViewAction.CheckAndUpdate: if ((viewState & ViewState.ChecksEnabled) && (viewState & (ViewState.Errored | ViewState.Destroyed)) === 0) { checkAndUpdateView(view); } }
Ahora sabe que el estado de la vista controla si se realiza la detección de cambios para esta vista y sus elementos secundarios o no. Entonces surge la pregunta: ¿podemos controlar ese estado? Resulta que podemos y de eso trata la segunda parte de este artículo.
Supongamos que tenemos el siguiente árbol de componentes:
Como aprendimos anteriormente, cada componente está asociado con una vista de componente. Cada vista se inicializa con ViewState.ChecksEnabled, lo que significa que cuando las ejecuciones angulares cambien la detección, se comprobarán todos los componentes del árbol.
Supongamos que queremos deshabilitar la detección de cambios para AComponent y sus hijos. Eso es fácil de hacer: solo necesitamos establecer ViewState.ChecksEnabled en falso. Cambiar de estado es una operación de bajo nivel, por lo que Angular nos proporciona un montón de métodos públicos disponibles en la vista. Cada componente puede obtener su vista asociada a través del token ChangeDetectorRef. Para esta clase, los documentos angulares definen la siguiente interfaz pública:
class ChangeDetectorRef { markForCheck() : void detach() : void reattach() : void detectChanges() : void checkNoChanges() : void }
Veamos qué podemos disputar para nuestro beneficio.
El primer método que nos permite manipular el estado es separar, que simplemente deshabilita las comprobaciones de la vista actual:
detach(): void { this._view.state &= ~ViewState.ChecksEnabled; }
Veamos cómo se puede usar en el código:
export class AComponent { constructor(public cd: ChangeDetectorRef) { this.cd.detach(); }
Esto asegura que durante las siguientes ejecuciones de detección de cambios, se omitirá la rama izquierda que comienza con AComponent (los componentes naranjas no se verificarán):
Hay dos cosas a tener en cuenta aquí: la primera es que, aunque cambiamos el estado de AComponent, todos sus componentes secundarios no se verificarán también. En segundo lugar, dado que no se realizará la detección de cambios para los componentes de la rama izquierda, el DOM en sus plantillas tampoco se actualizará. Aquí está el pequeño ejemplo para demostrarlo:
@Component({ selector: 'a-comp', template: `<span>See if I change: {{changed}}</span>` }) export class AComponent { constructor(public cd: ChangeDetectorRef) { this.changed = 'false'; setTimeout(() => { this.cd.detach(); this.changed = 'true'; }, 2000); }
La primera vez que se comprueba el componente, el lapso se representará con el texto Ver si cambio: falso. Y dentro de dos segundos, cuando la propiedad cambiada se actualiza a verdadero, el texto en el lapso no se cambiará. Sin embargo, si eliminamos esta línea this.cd.detach(), todo funcionará como se esperaba.
Como se muestra en la primera parte del artículo, el enlace del ciclo de vida de OnChanges aún se activará para AComponent si la entrada vincula cambios de aProp en AppComponent. Esto significa que una vez que se nos notifica que las propiedades de entrada cambian, podemos activar el detector de cambios para que el componente actual ejecute la detección de cambios y lo separe en el siguiente tick. Aquí está el fragmento que demuestra que:
export class AComponent { @Input() inputAProp; constructor(public cd: ChangeDetectorRef) { this.cd.detach(); } ngOnChanges(values) { this.cd.reattach(); setTimeout(() => { this.cd.detach(); }) }
Dado que volver a conectar simplemente establece el bit ViewState.ChecksEnabled:
reattach(): void { this._view.state |= ViewState.ChecksEnabled; }
esto es casi equivalente a lo que se hace cuando ChangeDetectionStrategy se establece en OnPush: deshabilite la verificación después de la primera ejecución de detección de cambios, habilítela cuando cambie la propiedad enlazada del componente principal y desactívela después de la ejecución.
Tenga en cuenta que el enlace OnChanges solo se activa para el componente superior en la rama deshabilitada, no para todos los componentes de la rama deshabilitada.
El método de reconexión habilita las comprobaciones solo para el componente actual, pero si la detección de cambios no está habilitada para su componente principal, no tendrá ningún efecto. Significa que el método de reconexión solo es útil para el componente superior en la rama deshabilitada.
Necesitamos una forma de habilitar la verificación de todos los componentes principales hasta el componente raíz. Y hay un método para ello markForCheck:
let currView: ViewData|null = view; while (currView) { if (currView.def.flags & ViewFlags.OnPush) { currView.state |= ViewState.ChecksEnabled; } currView = currView.viewContainerParent || currView.parent; }
Como puede ver en la implementación, simplemente itera hacia arriba y habilita las comprobaciones de cada componente principal hasta la raíz.
Hay una forma de ejecutar la detección de cambios una vez para el componente actual y todos sus elementos secundarios. Esto se hace usando el método detectChanges. Este método ejecuta la detección de cambios para la vista del componente actual independientemente de su estado, lo que significa que las comprobaciones pueden permanecer deshabilitadas para la vista actual y el componente no se comprobará durante las siguientes ejecuciones regulares de detección de cambios. Aquí hay un ejemplo:
export class AComponent { @Input() inputAProp; constructor(public cd: ChangeDetectorRef) { this.cd.detach(); } ngOnChanges(values) { this.cd.detectChanges(); }
El DOM se actualiza cuando cambia la propiedad de entrada aunque la referencia del detector de cambios permanezca desconectada.
Este último método disponible en el detector de cambios garantiza que no se realizarán cambios en la ejecución actual de detección de cambios. Básicamente, realiza 1,7,8 operaciones de la lista del primer artículo y genera una excepción si encuentra un enlace modificado o determina que DOM debe actualizarse.
Si has llegado hasta aquí, probablemente haya escrito un artículo útil. No dudes en recomendarlo . Soy el tipo al que le gusta profundizar para entender las cosas. Entonces, si desea recibir una notificación cada vez que encuentre información que valga la pena compartir, síganos . ¡Gracias!