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


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

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

Encontré que hay Vigilantes de JavaScript eso puede hacer este trabajo. Pero no son compatibles en Internet Explorer 6 y Internet Explorer 7. Entonces, ¿cómo sabe AngularJS que cambié, por ejemplo, lo siguiente y reflejé este cambio en una vista?

myobject.myproperty="new value";

1803
2018-03-13 10:16


origen


Respuestas:


AngularJS recuerda el valor y lo compara con un valor anterior. Esto es un control sucio básico. Si hay un cambio en el valor, se activa el evento de cambio.

los $apply() método, que es lo que llama cuando está pasando de un mundo que no es AngularJS a un mundo AngularJS, llamadas $digest(). Un resumen es simplemente viejo y sucio. Funciona en todos los navegadores y es totalmente predecible.

Para contrastar el control sucio (AngularJS) frente a los oyentes de cambio (KnockoutJS y Backbone.js): Aunque la verificación sucia puede parecer simple e incluso ineficaz (trataré eso más adelante), resulta que es semánticamente correcta todo el tiempo, mientras que los oyentes de cambio tienen muchos casos de esquina extraños y necesitan cosas como el seguimiento de la dependencia para hacer es más semánticamente correcto. El seguimiento de la dependencia KnockoutJS es una característica inteligente para un problema que AngularJS no tiene.

Problemas con los oyentes de cambio:

  • La sintaxis es atroz, ya que los navegadores no lo admiten de forma nativa. Sí, hay proxies, pero no son semánticamente correctos en todos los casos, y por supuesto no hay proxies en navegadores antiguos. La conclusión es que el control sucio le permite hacer POJO, mientras que KnockoutJS y Backbone.js te obligan a heredar de sus clases y acceder a tus datos a través de los usuarios.
  • Cambiar la fusión. Supongamos que tiene una serie de elementos. Supongamos que desea agregar elementos a una matriz, ya que está creando un bucle para agregar, cada vez que agregue está activando eventos en el cambio, que representa la interfaz de usuario. Esto es muy malo para el rendimiento. Lo que desea es actualizar la interfaz de usuario una sola vez al final. Los eventos de cambio son demasiado finos.
  • Los oyentes cambian inmediatamente de setter, lo cual es un problema, ya que el oyente de cambios puede cambiar aún más los datos, lo que dispara más eventos de cambio. Esto es malo ya que en su pila puede tener varios eventos de cambio sucediendo a la vez. Supongamos que tiene dos matrices que deben mantenerse sincronizadas por el motivo que sea. Solo puedes agregar uno o el otro, pero cada vez que agregas disparas un evento de cambio, que ahora tiene una vista incoherente del mundo. Este es un problema muy similar al bloqueo de subprocesos, que JavaScript evita ya que cada devolución de llamada se ejecuta de forma exclusiva y hasta su finalización. Los eventos de cambio rompen esto ya que los setters pueden tener consecuencias de largo alcance que no son intencionales y no obvias, lo que crea el problema de subprocesos nuevamente. Resulta que lo que desea hacer es retrasar la ejecución del oyente y garantizar que solo un oyente se ejecute a la vez, por lo tanto, cualquier código es libre de cambiar los datos, y sabe que no se ejecuta ningún otro código mientras lo hace. .

¿Qué hay del rendimiento?

Entonces, puede parecer que somos lentos, ya que el control sucio es ineficiente. Aquí es donde tenemos que mirar los números reales en lugar de solo tener argumentos teóricos, pero primero definamos algunas restricciones.

Los humanos son:

  • Lento - Cualquier cosa más rápida que 50 ms es imperceptible para los humanos y por lo tanto se puede considerar como "instantánea".

  • Limitado - Realmente no puedes mostrar más de 2000 piezas de información a un ser humano en una sola página. Cualquier cosa más que eso es realmente una IU mala, y los humanos no pueden procesar esto de todos modos.

Entonces la verdadera pregunta es esta: ¿Cuántas comparaciones puede hacer en un navegador en 50 ms? Esta es una pregunta difícil de responder ya que entran en juego muchos factores, pero aquí hay un caso de prueba: http://jsperf.com/angularjs-digest/6 que crea 10,000 espectadores. En un navegador moderno esto toma poco menos de 6 ms. En Internet Explorer 8 toma alrededor de 40 ms. Como puede ver, esto no es un problema incluso en navegadores lentos estos días. Hay una advertencia: las comparaciones deben ser simples para ajustarse al límite de tiempo ... Desafortunadamente es demasiado fácil agregar una comparación lenta en AngularJS, por lo que es fácil construir aplicaciones lentas cuando no sabes lo que estás haciendo. Pero esperamos tener una respuesta al proporcionar un módulo de instrumentación, que le mostraría cuáles son las comparaciones lentas.

Resulta que los videojuegos y las GPU usan el enfoque de control sucio, específicamente porque es consistente. Siempre y cuando superen la frecuencia de actualización del monitor (normalmente 50-60 Hz o cada 16.6-20 ms), cualquier rendimiento por encima de eso es un desperdicio, por lo que es mejor que dibuje más cosas, que aumentar el FPS.


2661
2018-03-13 23:47



Misko ya dio una excelente descripción de cómo funcionan los enlaces de datos, pero me gustaría agregar mi punto de vista sobre el problema de rendimiento con el enlace de datos.

Como dijo Misko, alrededor de 2000 enlaces es donde comienzas a ver problemas, pero de todos modos no debes tener más de 2000 piezas de información en una página. Esto puede ser cierto, pero no todos los enlaces de datos son visibles para el usuario. Una vez que comienzas a construir cualquier tipo de widget o cuadrícula de datos con enlace bidireccional, puedes fácilmente pulse 2000 bindings, sin tener un mal ux.

Considere, por ejemplo, un cuadro combinado donde puede escribir texto para filtrar las opciones disponibles. Este tipo de control podría tener ~ 150 elementos y aún así ser altamente utilizable. Si tiene alguna característica adicional (por ejemplo, una clase específica en la opción actualmente seleccionada), comienza a obtener 3-5 enlaces por opción. Ponga tres de estos widgets en una página (por ejemplo, uno para seleccionar un país, el otro para seleccionar una ciudad en dicho país y el tercero para seleccionar un hotel) y ya tiene entre 1000 y 2000 enlaces.

O considere una cuadrícula de datos en una aplicación web corporativa. 50 filas por página no es irrazonable, cada una de las cuales podría tener 10-20 columnas. Si construye esto con ng-repeats, y / o tiene información en algunas celdas que utiliza algunos enlaces, podría estar acercándose a los enlaces 2000 con esta cuadrícula sola.

Encuentro que esto es un enorme Problema al trabajar con AngularJS, y la única solución que he podido encontrar hasta ahora es construir widgets sin usar enlace bidireccional, en cambio usar ngOnce, anular el registro de vigilantes y trucos similares, o construir directivas que construyan el DOM con jQuery y Manipulación DOM Siento que esto derrota el propósito de usar Angular en primer lugar.

Me encantaría escuchar sugerencias sobre otras formas de manejar esto, pero quizás debería escribir mi propia pregunta. Quería poner esto en un comentario, pero resultó ser demasiado largo para eso ...

TL; DR 
El enlace de datos puede causar problemas de rendimiento en páginas complejas.


308
2017-08-22 13:28



Al revisar suciamente $scope objeto

Angular mantiene un simple array de observadores en el $scope objetos. Si inspecciona alguna $scope encontrará que contiene un array llamado $$watchers.

Cada observador es un object que contiene, entre otras cosas

  1. Una expresión que el observador está monitoreando. Esto podría ser solo un attribute nombre, o algo más complicado.
  2. Un último valor conocido de la expresión. Esto se puede comparar con el valor calculado actual de la expresión. Si los valores difieren, el observador activará la función y marcará $scope como sucio
  3. Una función que se ejecutará si el observador está sucio.

Cómo se definen los observadores

Hay muchas formas diferentes de definir un observador en AngularJS.

  • Puedes explícitamente $watch un attribute en $scope.

    $scope.$watch('person.username', validateUnique);
    
  • Puedes colocar un {{}} interpolación en su plantilla (un observador se creará para usted en la corriente $scope)

    <p>username: {{person.username}}</p>
    
  • Puede pedir una directiva como ng-model para definir al observador por ti

    <input ng-model="person.username" />
    

los $digest ciclo comprueba todos los observadores contra su último valor

Cuando interactuamos con AngularJS a través de los canales normales (ng-model, ng-repeat, etc.), la directiva activará un ciclo de resumen.

Un ciclo de digestión es un recorrido transversal en profundidad de $scope y todos sus niños. Para cada $scope  object, iteramos sobre su $$watchers  array y evalúa todas las expresiones. Si el nuevo valor de expresión es diferente del último valor conocido, se llama a la función del observador. Esta función puede recompilar parte del DOM, recalcular un valor en $scope, desencadenar un AJAX  requestcualquier cosa que necesites hacer

Cada alcance se recorre y cada expresión de reloj se evalúa y compara con el último valor.

Si se activa un vigilante, el $scope es sucio

Si se activa un vigilante, la aplicación sabe que algo ha cambiado, y el $scope está marcado como sucio

Las funciones de Watcher pueden cambiar otros atributos en $scope o en un padre $scope. Si uno $watcher función se ha activado, no podemos garantizar que nuestro otro $scopes todavía están limpios, y entonces ejecutamos todo el ciclo de digestión nuevamente.

Esto se debe a que AngularJS tiene enlace bidireccional, por lo que los datos pueden pasarse de nuevo $scopeárbol. Podemos cambiar un valor en un nivel superior $scope eso ya ha sido digerido Tal vez cambiemos un valor en el $rootScope.

Si el $digest está sucio, ejecutamos la totalidad $digest ciclo de nuevo

Continuamente recorremos el $digest ciclo hasta que el ciclo de digestión salga limpio (todo $watch las expresiones tienen el mismo valor que tenían en el ciclo anterior), o alcanzamos el límite de digestión. Por defecto, este límite está establecido en 10.

Si alcanzamos el límite de digestión, AngularJS generará un error en la consola:

10 $digest() iterations reached. Aborting!

El resumen es duro para la máquina pero fácil para el desarrollador

Como puede ver, cada vez que algo cambia en una aplicación AngularJS, AngularJS verificará a cada observador en el $scope jerarquía para ver cómo responder. Para un desarrollador, esta es una ventaja enorme de productividad, ya que ahora no necesita escribir casi ningún código de cableado, AngularJS solo notará si un valor ha cambiado, y hará que el resto de la aplicación sea consistente con el cambio.

Desde la perspectiva de la máquina, sin embargo, esto es tremendamente ineficiente y ralentizará nuestra aplicación si creamos demasiados observadores. Misko ha citado una cifra de alrededor de 4000 observadores antes de que tu aplicación se sienta lenta en los navegadores más antiguos.

Este límite es fácil de alcanzar si ng-repeat sobre un gran JSON  array por ejemplo. Puede mitigar esto utilizando funciones como el enlace de una sola vez para compilar una plantilla sin crear observadores.

Cómo evitar crear demasiados observadores

Cada vez que su usuario interactúa con su aplicación, cada observador en su aplicación será evaluado al menos una vez. Una gran parte de la optimización de una aplicación AngularJS es la reducción del número de observadores en su $scope árbol. Una manera fácil de hacer esto es con una vez vinculante.

Si tiene datos que rara vez cambiarán, puede enlazarlos solo una vez con la sintaxis ::, de esta forma:

<p>{{::person.username}}</p>

o

<p ng-bind="::person.username"></p>

La vinculación solo se activará cuando se represente la plantilla que lo contiene y los datos cargados en $scope.

Esto es especialmente importante cuando tienes un ng-repeat con muchos artículos

<div ng-repeat="person in people track by username">
  {{::person.username}}
</div>

142
2018-06-02 12:31



Esta es mi comprensión básica. ¡Bien puede estar mal!

  1. Los elementos se miran pasando una función (devolviendo lo que se va a visto) a la $watch método.
  2. Los cambios en los elementos observados deben realizarse dentro de un bloque de código envuelto por el $apply método.
  3. Al final de $apply el $digest método se invoca que va a través de cada uno de los relojes y controles para ver si han cambiado desde la última vez que $digest corrió.
  4. Si se encuentran cambios, se vuelve a invocar el resumen hasta que todos los cambios se estabilicen.

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


77
2018-03-13 21:01



Me lo pregunté por un tiempo. Sin setters, ¿cómo AngularJS aviso cambios a la $scope ¿objeto? ¿Los encuesta?

Lo que realmente hace es esto: cualquier lugar "normal" donde modifiques el modelo ya fue llamado desde las entrañas de AngularJS, entonces llama automáticamente $apply para usted después de que se ejecute su código. Digamos que su controlador tiene un método que está conectado a ng-click en algún elemento. Porque AngularJS envía la llamada de ese método para ti, tiene la oportunidad de hacer un $apply en el lugar apropiado. Del mismo modo, para las expresiones que aparecen en las vistas, las ejecuta AngularJS entonces hace el $apply.

Cuando la documentación habla de tener que llamar $apply manualmente para el código fuera de AngularJS, se trata de un código que, cuando se ejecuta, no se deriva de AngularJS en la pila de llamadas.


57
2017-09-03 17:45



Explicando con imágenes:

Data-Binding necesita un mapeo

La referencia en el alcance no es exactamente la referencia en la plantilla. Cuando vincula dos objetos con datos, necesita un tercero que escuche el primero y modifique el otro.

enter image description here

Aquí, cuando modificas el <input>, tocas el data-ref3. Y el mecanismo de enlace de datos clásico cambiará data-ref4. Entonces, ¿cómo el otro {{data}} las expresiones se moverán?

Los eventos llevan a $ digest ()

enter image description here

Angular mantiene un oldValue y newValue de cada enlace. Y después de cada Evento angular, el famoso $digest() loop verificará WatchList para ver si algo cambió. Estas Eventos angulares son ng-click, ng-change, $http completado ... El $digest() se repetirá siempre que cualquier oldValue difiere de la newValue.

En la imagen anterior, notará que data-ref1 y data-ref2 han cambiado.

Conclusiones

Es un poco como el huevo y el pollo. Nunca se sabe quién comienza, pero con suerte funciona la mayor parte del tiempo como se esperaba.

El otro punto es que puedes entender fácilmente el impacto profundo de un enlace simple en la memoria y la CPU. Con suerte, los equipos de sobremesa son lo suficientemente gordos como para manejar esto. Los teléfonos móviles no son tan fuertes.


29
2018-05-20 13:33



Obviamente no hay verificación periódica de Scope si hay algún cambio en los Objetos adjuntos. No se ven todos los objetos adjuntos al alcance. El alcance prototípicamente mantiene una $$ vigilantes . Scope solo itera a través de este $$watchers cuando $digest se llama .

Angular agrega un observador a los $$ watchers para cada uno de estos

  1. {{expression}} - En sus plantillas (y en cualquier otro lugar donde exista una expresión) o cuando definimos ng-model.
  2. $ scope. $ watch ('expresión / función') - En tu JavaScript solo podemos adjuntar un objeto de alcance para angular para mirar.

$ reloj la función toma en tres parámetros:

  1. El primero es una función de observador que simplemente devuelve el objeto o simplemente podemos agregar una expresión.

  2. El segundo es una función de escucha que se llamará cuando haya un cambio en el objeto. Todas las cosas como los cambios DOM se implementarán en esta función.

  3. El tercero es un parámetro opcional que toma un booleano. Si es verdadera, la profundidad angular observa el objeto y si su falso angular solo hace una referencia para observar el objeto.     La implementación aproximada de $ watch se ve así

Scope.prototype.$watch = function(watchFn, listenerFn) {
   var watcher = {
       watchFn: watchFn,
       listenerFn: listenerFn || function() { },
       last: initWatchVal  // initWatchVal is typically undefined
   };
   this.$$watchers.push(watcher); // pushing the Watcher Object to Watchers  
};

Hay algo interesante en Angular llamado Digest Cycle. El ciclo $ digest comienza como resultado de una llamada a $ scope. $ Digest (). Supongamos que cambia un modelo $ scope en una función de controlador a través de la directiva ng-click. En ese caso, AngularJS activa automáticamente un ciclo $ digest llamando a $ digest (). Además de ng-click, hay varias otras directivas / servicios incorporados que le permiten cambiar los modelos (por ejemplo, ng-model, $ timeout, etc.) y activa automáticamente un ciclo $ digest. La implementación aproximada de $ digest se ve así.

Scope.prototype.$digest = function() {
      var dirty;
      do {
          dirty = this.$$digestOnce();
      } while (dirty);
}
Scope.prototype.$$digestOnce = function() {
   var self = this;
   var newValue, oldValue, dirty;
   _.forEach(this.$$watchers, function(watcher) {
          newValue = watcher.watchFn(self);
          oldValue = watcher.last;   // It just remembers the last value for dirty checking
          if (newValue !== oldValue) { //Dirty checking of References 
   // For Deep checking the object , code of Value     
   // based checking of Object should be implemented here
             watcher.last = newValue;
             watcher.listenerFn(newValue,
                  (oldValue === initWatchVal ? newValue : oldValue),
                   self);
          dirty = true;
          }
     });
   return dirty;
 };

Si usamos JavaScript setTimeout () función para actualizar un modelo de alcance, Angular no tiene forma de saber lo que puede cambiar. En este caso, es nuestra responsabilidad llamar $ apply () manualmente, lo que desencadena un ciclo $ digest. De manera similar, si tiene una directiva que configura un detector de eventos DOM y cambia algunos modelos dentro de la función de controlador, debe llamar a $ apply () para asegurarse de que los cambios entren en vigencia. La gran idea de $ apply es que podemos ejecutar algún código que no tenga en cuenta Angular, ese código aún puede cambiar las cosas en el alcance. Si envolvemos ese código en $ apply, se ocupará de llamar a $ digest (). Implementación aproximada de $ apply ().

Scope.prototype.$apply = function(expr) {
       try {
         return this.$eval(expr); //Evaluating code in the context of Scope
       } finally {
         this.$digest();
       }
};

19
2018-05-22 18:18