¿Cómo funciona el enlace de datos en AngularJS?

¿Cómo funciona la vinculación de datos en el marco AngularJS?

No he encontrado detalles técnicos en su sitio. Está más o menos claro cómo funciona cuando los datos se propagan de la vista al modelo. Pero, ¿cómo hace AngularJS para seguir los cambios de las propiedades del modelo sin setters y getters?

He encontrado que hay JavaScript watchers que pueden hacer este trabajo. Pero no están soportados en Internet Explorer 6 y Internet Explorer 7. Entonces, ¿cómo sabe AngularJS que he cambiado, por ejemplo, lo siguiente y ha reflejado este cambio en una vista?

myobject.myproperty="new value";
Solución

AngularJS recuerda el valor y lo compara con un valor anterior. Esto es una comprobación básica de la suciedad. Si hay un cambio en el valor, entonces dispara el evento de cambio.

El método $apply(), que es lo que se llama cuando se pasa de un mundo no-AngularJS a un mundo AngularJS, llama a $digest(). Un resumen es simplemente una comprobación sucia. Funciona en todos los navegadores y es totalmente predecible.

Para contrastar la comprobación sucia (AngularJS) frente a los escuchadores de cambios (KnockoutJS y Backbone.js): Mientras que el dirty-checking puede parecer simple, e incluso ineficiente (lo trataré más adelante), resulta que es semánticamente correcto todo el tiempo, mientras que los change listeners tienen muchos casos de esquina extraños y necesitan cosas como el seguimiento de dependencias para hacerlo más semánticamente correcto. El seguimiento de dependencias de KnockoutJS es una característica inteligente para un problema que AngularJS no tiene.

Problemas con los escuchadores de cambios:

  • La sintaxis es atroz, ya que los navegadores no la soportan de forma nativa. Sí, hay proxies, pero no son semánticamente correctos en todos los casos, y por supuesto no hay proxies en los navegadores antiguos. La conclusión es que el dirty-checking te permite hacer POJO, mientras que KnockoutJS y Backbone.js te obligan a heredar de sus clases, y a acceder a tus datos a través de los accessors.
  • Cambiar la coalescencia. Supongamos que tienes un array de elementos. Digamos que quieres añadir elementos en un array, mientras estás haciendo un bucle para añadir, cada vez que añades estás disparando eventos de cambio, lo que está renderizando la UI. Esto es muy malo para el rendimiento. Lo que quieres es actualizar la interfaz de usuario sólo una vez, al final. Los eventos de cambio son demasiado finos.
  • Los escuchadores de cambios se disparan inmediatamente en un setter, lo que es un problema, ya que el escuchador de cambios puede cambiar más datos, lo que dispara más eventos de cambio. Esto es malo ya que en tu pila puedes tener varios eventos de cambio sucediendo a la vez. Supongamos que tienes dos arrays que necesitan mantenerse sincronizados por cualquier razón. Sólo puedes añadir a uno o al otro, pero cada vez que añades disparas un evento de cambio, que ahora tiene una visión inconsistente del mundo. Este es un problema muy similar al bloqueo de hilos, que JavaScript evita ya que cada callback se ejecuta exclusivamente y hasta su finalización. Los eventos de cambio rompen esto ya que los setters pueden tener consecuencias de largo alcance que no están previstas y no son obvias, lo que crea el problema de los hilos de nuevo. Resulta que lo que se quiere hacer es retrasar la ejecución del listener, y garantizar, que sólo un listener se ejecuta a la vez, por lo que cualquier código es libre de cambiar los datos, y sabe que ningún otro código se ejecuta mientras lo hace.

¿Qué pasa con el rendimiento?

Puede parecer que somos lentos, ya que el dirty-checking es ineficiente. Aquí es donde tenemos que mirar los números reales en lugar de tener sólo argumentos teóricos, pero primero vamos a definir algunas restricciones.

Los humanos son:

Lentos - Todo lo que sea más rápido que 50 ms es imperceptible para los humanos y, por tanto, puede considerarse "instantáneo".

Limitado - Realmente no se puede mostrar más de unas 2000 piezas de información a un humano en una sola página. Todo lo que sea más que eso es una interfaz de usuario realmente mala, y los humanos no pueden procesar esto de todos modos.

Así que la verdadera pregunta es esta: ¿Cuántas comparaciones se pueden hacer en un navegador en 50 ms? Es una pregunta difícil de responder, ya que entran en juego muchos factores, pero he aquí un caso de prueba: http://jsperf.com/angularjs-digest/6 que crea 10.000 observadores. En un navegador moderno esto tarda algo menos de 6 ms. En Internet Explorer 8 tarda unos 40 ms. Como puedes ver, esto no es un problema ni siquiera en los navegadores lentos de hoy en día. Hay una advertencia: las comparaciones deben ser sencillas para ajustarse al límite de tiempo... Desgraciadamente es demasiado fácil añadir una comparación lenta en AngularJS, por lo que es fácil construir aplicaciones lentas cuando no se sabe lo que se está haciendo. Pero esperamos tener una respuesta proporcionando un módulo de instrumentación, que te mostraría cuáles son las comparaciones lentas.

Resulta que los videojuegos y las GPUs utilizan el enfoque de comprobación sucia, específicamente porque es consistente. Mientras superen la tasa de refresco del monitor (normalmente 50-60 Hz, o cada 16,6-20 ms), cualquier rendimiento por encima de eso es un desperdicio, así que es mejor dibujar más cosas, que conseguir más FPS.

Comentarios (38)

Este es mi entendimiento básico. Puede que esté equivocado.

  1. Los elementos se vigilan pasando una función (que devuelve la cosa a vigilar) al método a vigilar) al método $watch.
  2. Los cambios en los elementos vigilados deben realizarse dentro de un bloque de código envuelto por el método $apply.
  3. Al final de la aplicación se invoca el método $digest que recorre cada uno de los que recorre cada uno de los relojes y comprueba si han cambiado desde la última vez que se ejecutó el $digest.
  4. Si se encuentra algún cambio, se invoca de nuevo el digest hasta que se estabilicen todos los cambios.

En el desarrollo normal, la sintaxis de enlace de datos en el HTML le dice al compilador de AngularJS que cree los relojes por ti y los métodos del controlador ya se ejecutan dentro de $apply. Así que para el desarrollador de la aplicación es todo transparente.

Comentarios (4)

Yo mismo me he preguntado esto durante un tiempo. Sin setters, ¿cómo se da cuenta AngularJS de los cambios en el objeto $scope? ¿Los sondea?

Lo que hace en realidad es lo siguiente: Cualquier lugar "normal" en el que modifiques el modelo ya ha sido llamado desde las tripas de AngularJS, así que automáticamente llama a $apply por ti después de que tu código se ejecute. Digamos que tu controlador tiene un método que está conectado a ng-click en algún elemento. Como AngularJS conecta la llamada de ese método por ti, tiene la oportunidad de hacer una $apply en el lugar apropiado. Igualmente, para las expresiones que aparecen justo en las vistas, éstas son ejecutadas por AngularJS para que haga el $apply.

Cuando la documentación habla de tener que llamar a $apply manualmente para código fuera de AngularJS, está hablando de código que, cuando se ejecuta, no proviene del propio AngularJS en la pila de llamadas.

Comentarios (0)