angularjs-scope $apply - AngularJS:Previene el error $ digest ya en curso al llamar a $ scope.$ Apply()




$scope.$apply() $timeout (21)

Estoy encontrando que necesito actualizar mi página a mi alcance manualmente cada vez más desde la construcción de una aplicación en angular.

La única forma que conozco de hacer esto es llamar a $apply() desde el alcance de mis controladores y directivas. El problema con esto es que sigue arrojando un error a la consola que dice:

Error: $ digerido ya en progreso

¿Alguien sabe cómo evitar este error o lograr lo mismo pero de una manera diferente?


Answers

Muchas de las respuestas aquí contienen buenos consejos, pero también pueden llevar a confusión. Simplemente usar $timeout no es la mejor ni la solución correcta. Además, asegúrese de leer eso si está preocupado por el rendimiento o la escalabilidad.

Cosas que debes saber

  • $$phase es privada para el marco y hay buenas razones para ello.

  • $timeout(callback) esperará hasta que se complete el ciclo de compendio actual (si corresponde), luego ejecutará la devolución de llamada, luego ejecutará al final una aplicación completa de $apply .

  • $timeout(callback, delay, false) hará lo mismo (con un retraso opcional antes de ejecutar la devolución de llamada), pero no activará un $apply (tercer argumento) que guarda el rendimiento si no modificó su modelo Angular ($ scope ).

  • $scope.$apply(callback) invoca, entre otras cosas, $rootScope.$digest , lo que significa que redigerá el alcance raíz de la aplicación y todos sus elementos secundarios, incluso si está dentro de un alcance aislado.

  • $scope.$digest() simplemente sincronizará su modelo con la vista, pero no digerirá su alcance principal, lo que puede ahorrar un montón de actuaciones cuando se trabaja en una parte aislada de su HTML con un alcance aislado (principalmente de una directiva) . $ digest no recibe una devolución de llamada: usted ejecuta el código, luego digiere.

  • $scope.$evalAsync(callback) se ha introducido con angularjs 1.2, y probablemente resolverá la mayoría de sus problemas. Por favor, consulte el último párrafo para obtener más información al respecto.

  • Si obtiene el $digest already in progress error , entonces su arquitectura es incorrecta: no necesita redigir su alcance o no debería estar a cargo de eso (ver más abajo).

Cómo estructurar tu código

Cuando recibe ese error, intenta digerir su alcance mientras está en progreso: dado que no conoce el estado de su alcance en ese momento, no está a cargo de lidiar con su digestión.

function editModel() {
  $scope.someVar = someVal;
  /* Do not apply your scope here since we don't know if that
     function is called synchronously from Angular or from an
     asynchronous code */
}

// Processed by Angular, for instance called by a ng-click directive
$scope.applyModelSynchronously = function() {
  // No need to digest
  editModel();
}

// Any kind of asynchronous code, for instance a server request
callServer(function() {
  /* That code is not watched nor digested by Angular, thus we
     can safely $apply it */
  $scope.$apply(editModel);
});

Y si sabe lo que está haciendo y está trabajando en una directiva pequeña aislada mientras forma parte de una gran aplicación Angular, podría preferir $ digest en lugar de $ aplicar para guardar actuaciones.

Actualización desde Angularjs 1.2

Se ha agregado un método nuevo y poderoso a cualquier $ scope: $evalAsync . Básicamente, ejecutará su devolución de llamada dentro del ciclo de resumen actual si se está produciendo, de lo contrario, un nuevo ciclo de resumen comenzará a ejecutar la devolución de llamada.

Eso todavía no es tan bueno como un $scope.$digest Si realmente sabe que solo necesita sincronizar una parte aislada de su HTML (ya que se activará un nuevo $apply si no hay ninguno en curso), pero esto es lo mejor Solución cuando está ejecutando una función que no puede saber si se ejecutará de forma síncrona o no , por ejemplo, después de obtener un recurso potencialmente almacenado en caché: a veces esto requerirá una llamada asíncrona a un servidor, de lo contrario, el recurso se buscará de forma local y sincrónica.

En estos casos y en todos los demás en los que tuvo una !$scope.$$phase , asegúrese de usar $scope.$evalAsync( callback )


Consulte http://docs.angularjs.org/error/$rootScope:inprog

El problema surge cuando tiene una llamada a $apply que a veces se ejecuta de forma asíncrona fuera del código Angular (cuando se debe usar $ apply) y, a veces, sincrónicamente, dentro del código Angular (que causa el error $digest already in progress ).

Esto puede suceder, por ejemplo, cuando tiene una biblioteca que obtiene elementos de forma asíncrona de un servidor y los almacena en caché. La primera vez que se solicita un elemento, se recuperará de forma asíncrona para no bloquear la ejecución del código. Sin embargo, la segunda vez, el elemento ya está en la memoria caché, por lo que se puede recuperar sincrónicamente.

La forma de evitar este error es asegurarse de que el código que llama a $apply se ejecute de forma asíncrona. Esto se puede hacer ejecutando su código dentro de una llamada a $timeout con el retraso establecido en 0 (que es el valor predeterminado). Sin embargo, llamar a su código dentro de $timeout elimina la necesidad de llamar a $apply , porque $ timeout activará otro ciclo de $digest por sí solo, lo que, a su vez, hará todas las actualizaciones necesarias, etc.

Solución

En resumen, en lugar de hacer esto:

... your controller code...

$http.get('some/url', function(data){
    $scope.$apply(function(){
        $scope.mydate = data.mydata;
    });
});

... more of your controller code...

hacer esto:

... your controller code...

$http.get('some/url', function(data){
    $timeout(function(){
        $scope.mydate = data.mydata;
    });
});

... more of your controller code...

Solo llame a $apply cuando sepa que el código que se está ejecutando siempre se ejecutará fuera del código Angular (por ejemplo, su llamada a $ apply ocurrirá dentro de una devolución de llamada que se llama mediante un código fuera de su código Angular).

A menos que alguien esté al tanto de una desventaja impactante al usar $timeout sobre $apply , no veo por qué no siempre podría usar $timeout (sin demora) en lugar de $apply , ya que hará aproximadamente lo mismo.



Entendiendo que los documentos de Angular llaman a la comprobación de la $$phase y al anti-patrón , traté de obtener $timeout y _.defer to work.

El tiempo de espera y los métodos diferidos crean un flash de contenido {{myVar}} en el dom como un FOUT . Para mí esto no era aceptable. Me deja sin mucho que decir dogmáticamente que algo es un hack y que no tenga una alternativa adecuada.

Lo único que funciona cada vez es:

if(scope.$$phase !== '$digest'){ scope.$digest() } .

No entiendo el peligro de este método, o por qué se describe como un hack por parte de las personas en los comentarios y el equipo angular. El comando parece preciso y fácil de leer:

"Haz el resumen a menos que uno ya esté sucediendo"

En CoffeeScript es aún más bonito:

scope.$digest() unless scope.$$phase is '$digest'

¿Cuál es el problema con esto? ¿Hay alguna alternativa que no cree un FOUT? https://github.com/yearofmoo/AngularJS-Scope.SafeApply ve bien, pero también usa el método de inspección de $$phase .


A veces, aún obtendrá errores si usa esta forma ( https://.com/a/12859093/801426 ).

Prueba esto:

if(! $rootScope.$root.$$phase) {
...

No utilice este patrón : esto terminará causando más errores de los que resuelve. Aunque pienses que arregla algo, no lo hizo.

Puede verificar si un $digest ya está en curso marcando la $scope.$$phase .

if(!$scope.$$phase) {
  //$digest or $apply
}

$scope.$$phase devolverá "$digest" o "$apply" si un $digest o $apply está en progreso. Creo que la diferencia entre estos estados es que $digest procesará los relojes del alcance actual y sus hijos, y $apply procesará a los observadores de todos los ámbitos.

Para el punto de @dnc253, si te encuentras llamando $digest o $apply frecuencia, es posible que lo estés haciendo mal. En general, encuentro que necesito digerir cuando necesito actualizar el estado del alcance como resultado de un evento DOM que se dispara fuera del alcance de Angular. Por ejemplo, cuando un modo bootstrap de twitter se oculta. A veces, el evento DOM se dispara cuando hay un $digest en curso, a veces no. Por eso uso este cheque.

Me encantaría conocer una mejor manera si alguien la conoce.

De los comentarios: por @anddoutoi

angular.js Anti Patrones

  1. No lo haga if (!$scope.$$phase) $scope.$apply() , significa que $scope.$apply() no es lo suficientemente alto en la pila de llamadas.

También puede utilizar evalAsync. Se ejecutará en algún momento después de que el resumen ha terminado!

scope.evalAsync(function(scope){
    //use the scope...
});

Un método pequeño y práctico para mantener este proceso SECO:

function safeApply(scope, fn) {
    (scope.$$phase || scope.$root.$$phase) ? fn() : scope.$apply(fn);
}

usar $scope.$$phase || $scope.$apply(); $scope.$$phase || $scope.$apply(); en lugar


He podido resolver este problema llamando a $eval lugar de $apply en lugares donde sé que se ejecutará la función $digest .

Según los docs , $apply básicamente hace esto:

function $apply(expr) {
  try {
    return $eval(expr);
  } catch (e) {
    $exceptionHandler(e);
  } finally {
    $root.$digest();
  }
}

En mi caso, un ng-click cambia una variable dentro de un ámbito, y un $ watch en esa variable cambia otras variables que deben $applied . Este último paso provoca el error "compendio ya en progreso".

Al reemplazar $apply con $eval dentro de la expresión de observación, las variables de alcance se actualizan como se espera.

Por lo tanto, parece que si el resumen se va a ejecutar de cualquier manera debido a algún otro cambio en Angular, $eval 'ing es todo lo que necesita hacer.


Tuve el mismo problema con los scripts de terceros como CodeMirror, por ejemplo, y Krpano, e incluso utilizando los métodos de SafeApply mencionados aquí no he resuelto el error.

Pero lo que sí resolvió es usar el servicio de tiempo de espera de $ (no se olvide de inyectarlo primero).

Así, algo como:

$timeout(function() {
  // run my code safely here
})

y si dentro de tu código estás usando

esta

tal vez porque está dentro del controlador de una directiva de fábrica o simplemente necesita algún tipo de enlace, entonces usted haría algo como:

.factory('myClass', [
  '$timeout',
  function($timeout) {

    var myClass = function() {};

    myClass.prototype.surprise = function() {
      // Do something suprising! :D
    };

    myClass.prototype.beAmazing = function() {
      // Here 'this' referes to the current instance of myClass

      $timeout(angular.bind(this, function() {
          // Run my code safely here and this is not undefined but
          // the same as outside of this anonymous function
          this.surprise();
       }));
    }

    return new myClass();

  }]
)

Esto solucionará tu problema:

if(!$scope.$$phase) {
  //TODO
}

Encontré esto: https://coderwall.com/p/ngisma donde Nathan Walker (cerca de la parte inferior de la página) sugiere un decorador en $ rootScope para crear la función 'safeApply', código:

yourAwesomeModule.config([
  '$provide', function($provide) {
    return $provide.decorator('$rootScope', [
      '$delegate', function($delegate) {
        $delegate.safeApply = function(fn) {
          var phase = $delegate.$$phase;
          if (phase === "$apply" || phase === "$digest") {
            if (fn && typeof fn === 'function') {
              fn();
            }
          } else {
            $delegate.$apply(fn);
          }
        };
        return $delegate;
      }
    ]);
  }
]);

Cuando recibe este error, básicamente significa que ya está en proceso de actualizar su vista. Realmente no debería necesitar llamar a $apply() desde su controlador. Si su vista no se actualiza como esperaría, y luego recibe este error después de llamar a $apply() , lo más probable es que no esté actualizando el modelo correctamente. Si publicas algunos detalles, podríamos resolver el problema central.


De una discusión reciente con los chicos de Angular sobre este mismo tema: por razones de pruebas futuras, no debe usar la $$phase

Cuando se presiona para la forma "correcta" de hacerlo, la respuesta es actualmente

$timeout(function() {
  // anything you want can go here and will safely be run on the next digest.
})

Recientemente me encontré con esto al escribir servicios angulares para envolver las API de Facebook, Google y Twitter que, en diversos grados, han recibido devoluciones de llamadas.

Aquí hay un ejemplo desde dentro de un servicio. (En aras de la brevedad, el resto del servicio, que configuró las variables, inyectó $ timeout, etc., se ha dejado de lado).

window.gapi.client.load('oauth2', 'v2', function() {
    var request = window.gapi.client.oauth2.userinfo.get();
    request.execute(function(response) {
        // This happens outside of angular land, so wrap it in a timeout 
        // with an implied apply and blammo, we're in action.
        $timeout(function() {
            if(typeof(response['error']) !== 'undefined'){
                // If the google api sent us an error, reject the promise.
                deferred.reject(response);
            }else{
                // Resolve the promise with the whole response if ok.
                deferred.resolve(response);
            }
        });
    });
});

Tenga en cuenta que el argumento de demora para $ timeout es opcional y se establecerá de manera predeterminada en 0 si no se configura ( $timeout calls calls $browser.defer .

Un poco no intuitivo, pero esa es la respuesta de los chicos que escriben Angular, ¡así que es lo suficientemente bueno para mí!


La forma más corta de $apply seguro es:

$timeout(angular.noop)

Este es mi servicio de utilidad.

angular.module('myApp', []).service('Utils', function Utils($timeout) {
    var Super = this;

    this.doWhenReady = function(scope, callback, args) {
        if(!scope.$$phase) {
            if (args instanceof Array)
                callback.apply(scope, Array.prototype.slice.call(args))
            else
                callback();
        }
        else {
            $timeout(function() {
                Super.doWhenReady(scope, callback, args);
            }, 250);
        }
    };
});

Y este es un ejemplo de su uso:

angular.module('myApp').controller('MyCtrl', function ($scope, Utils) {
    $scope.foo = function() {
        // some code here . . .
    };

    Utils.doWhenReady($scope, $scope.foo);

    $scope.fooWithParams = function(p1, p2) {
        // some code here . . .
    };

    Utils.doWhenReady($scope, $scope.fooWithParams, ['value1', 'value2']);
};

Le aconsejaría que utilice un evento personalizado en lugar de desencadenar un ciclo de resumen.

Descubrí que transmitir eventos personalizados y registrar escuchas para estos eventos es una buena solución para desencadenar una acción que desea que ocurra, ya sea que se encuentre o no en un ciclo de resumen.

Al crear un evento personalizado, también está siendo más eficiente con su código porque solo está activando escuchas suscritas a dicho evento y NO está activando todos los relojes vinculados al alcance como lo haría si invocara el alcance. $ Apply.

$scope.$on('customEventName', function (optionalCustomEventArguments) {
   //TODO: Respond to event
});


$scope.$broadcast('customEventName', optionalCustomEventArguments);

He estado usando este método y parece funcionar perfectamente bien. Esto solo espera el momento en que el ciclo ha terminado y luego los activadores se apply() . Simplemente llame a la función apply(<your scope>) desde cualquier lugar que desee.

function apply(scope) {
  if (!scope.$$phase && !scope.$root.$$phase) {
    scope.$apply();
    console.log("Scope Apply Done !!");
  } 
  else {
    console.log("Scheduling Apply after 200ms digest cycle already in progress");
    setTimeout(function() {
        apply(scope)
    }, 200);
  }
}

yearofmoo hizo un gran trabajo al crear una función $ safeApply reutilizable para nosotros:

https://github.com/yearofmoo/AngularJS-Scope.SafeApply

Uso:

//use by itself
$scope.$safeApply();

//tell it which scope to update
$scope.$safeApply($scope);
$scope.$safeApply($anotherScope);

//pass in an update function that gets called when the digest is going on...
$scope.$safeApply(function() {

});

//pass in both a scope and a function
$scope.$safeApply($anotherScope,function() {

});

//call it on the rootScope
$rootScope.$safeApply();
$rootScope.$safeApply($rootScope);
$rootScope.$safeApply($scope);
$rootScope.$safeApply($scope, fn);
$rootScope.$safeApply(fn);

Una de las decisiones de diseño detrás de la introducción de ámbitos fue facilitar la administración de la memoria. Al dividir el espacio del modelo en subpartes (ámbitos) podemos eliminar las partes innecesarias del modelo (ámbito) y agregar nuevas cuando sea necesario. Así que sí, los ámbitos son una parte importante de todo el rompecabezas de la gestión de la memoria.

Cuando se trata de su pregunta específica sobre ng-view , esta directiva mantendrá el alcance solo para la vista actualmente activa . ng-view es una de las directivas de creación (¡y destrucción de alcance!). Creará automáticamente un nuevo ámbito cuando se navegue por una nueva vista y destruirá automáticamente un ámbito conectado con la vista anterior. Esto se puede verificar fácilmente en el código fuente de AngularJS.

La única parte que consume memoria a considerar son las plantillas que se obtienen a través de una red. Todas las plantillas a las que se hace referencia en una ruta se almacenan en la caché de $templateCache . Puede desalojar las plantillas con moderación si determina que aborda un cuello de botella específico en su aplicación. Solo debemos darnos cuenta de que es tiempo de intercambio (tiempo de red) para el consumo de memoria.

En resumen: no es necesario desplegar su propia administración de alcance para la ng-view : si ve alguna retención de alcance, debe informarse como un error.







angularjs angularjs-scope angular-digest