Pregunta AngularJS: evita el error $ digest que ya está en progreso al llamar a $ scope. $ Apply ()


Estoy descubriendo que necesito actualizar mi página a mi alcance manualmente más y más desde que construí una aplicación en angular.

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

Error: $ digest ya está en progreso

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


784
2017-10-04 14:07


origen


Respuestas:


No use este patrón - Esto terminará causando más errores de los que resuelve. Aunque piensas que arregló algo, no lo hizo.

Puedes verificar si $digest ya está en progreso comprobando $scope.$$phase.

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

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

Para el punto de @ dnc253, si te encuentras llamando $digest o $apply con frecuencia, puede estar haciendo mal. En general, creo 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 modal de arranque de Twitter se oculta. Algunas veces el evento DOM se dispara cuando $digest está en progreso, a veces no. Es por eso que uso este cheque.

Me encantaría saber una mejor manera si alguien sabe una.


De los comentarios: por @anddoutoi

angular.js Anti Patrones

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

635
2017-10-12 12:28



De una discusión reciente con los chicos de Angular sobre este mismo tema: Por razones de futuro, no deberías usar $$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 devuelto las devoluciones de llamada.

Aquí hay un ejemplo de un servicio. (En aras de la brevedad, el resto del servicio, que configura variables, inyectado $ tiempo de espera, etc.) se ha dejado de usar.

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á en 0 por defecto si no se establece ($ tiempo de espera llamadas $ browser.defer cual se pone por defecto a 0 si el retraso no está establecido)

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


631
2017-09-25 04:06



El ciclo de resumen es una llamada sincrónica. No cederá el control del ciclo de eventos del navegador hasta que esté listo. Hay algunas formas de lidiar con esto. La forma más fácil de manejar esto es utilizar el tiempo de espera $ incorporado, y una segunda forma es si usa subrayado o lodash (y debe hacerlo), llame a lo siguiente:

$timeout(function(){
    //any code in here will automatically have an apply run afterwards
});

o si tiene un guión bajo:

_.defer(function(){$scope.$apply();});

Probamos varias soluciones y odiamos inyectar $ rootScope en todos nuestros controladores, directivas e incluso algunas fábricas. Entonces, $ timeout y _.defer han sido nuestros favoritos hasta ahora. Estos métodos indican con éxito a angular esperar hasta el siguiente ciclo de animación, lo que garantizará que el alcance actual. $ Apply haya terminado.


312
2017-07-30 22:51



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

Cosas que deberías saber

  • $$phase es privado para el marco y hay buenas razones para eso.

  • $timeout(callback) esperará hasta que se complete el ciclo de resumen actual (si lo hay), luego ejecutará la devolución de llamada, luego ejecutará al final un ciclo completo $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 ahorra actuaciones si no modificó su modelo angular ($ scope).

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

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

  • $scope.$evalAsync(callback) ha sido presentado con angularjs 1.2, y probablemente resuelva la mayoría de tus problemas. Consulte el último párrafo para obtener más información al respecto.

  • si obtienes el $digest already in progress error, entonces su arquitectura está equivocada: o bien no es necesario reajustar su alcance, o no deberías estar a cargo de eso (vea abajo).

Cómo estructurar tu código

Cuando obtiene ese error, está tratando de digerir su alcance mientras ya está en curso: ya que no conoce el estado de su alcance en ese punto, 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 pequeña directiva aislada mientras forma parte de una gran aplicación angular, podría preferir $ digest en lugar de $ apply para guardar actuaciones.

Actualización desde Angularjs 1.2

Un nuevo y poderoso método ha sido agregado a cualquier $ scope: $evalAsync. Básicamente, ejecutará su devolución de llamada dentro del ciclo de resumen actual si está ocurriendo, 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 una nueva $apply se activará si no hay ninguno en progreso), pero esta es la mejor solución cuando está ejecutando una función que no puedes saber si se ejecutará sincrónicamente o no, por ejemplo, después de buscar un recurso potencialmente en caché: a veces esto requerirá una llamada asincrónica a un servidor; de lo contrario, el recurso se obtendrá localmente de forma síncrona.

En estos casos y todos los demás donde tenías un !$scope.$$phase, asegúrate de usar $scope.$evalAsync( callback )


257
2018-04-16 06:59



Práctico método de ayuda pequeña para mantener este proceso SECO:

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

86
2018-06-14 18:14



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

El problema surge cuando tienes 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 $digest already in progresserror).

Esto puede suceder, por ejemplo, cuando tiene una biblioteca que obtiene de manera asincrónica elementos 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. La segunda vez, sin embargo, el elemento ya está en caché, por lo que se puede recuperar de forma sincronizada.

La forma de evitar este error es garantizar que el código que llama $apply se ejecuta de forma asincrónica. Esto se puede hacer ejecutando su código dentro de una llamada a $timeout con la demora establecida en 0 (que es el valor predeterminado). Sin embargo, llamando a su código dentro $timeout elimina la necesidad de llamar $apply, porque $ timeout activará otro $digest ciclo por sí mismo, 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 llamar $apply cuando sepa que el código que se ejecuta 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 por código fuera de su código angular).

A menos que alguien sea consciente de alguna desventaja impactante al usar $timeout encima $apply, No veo por qué no siempre podrías usar $timeout (con cero retraso) en lugar de $apply, ya que hará aproximadamente lo mismo.


31
2018-01-21 21:33



Tuve el mismo problema con los guiones de terceros como CodeMirror, por ejemplo, y Krpano, e incluso el uso de los métodos safeApply mencionados aquí no me han solucionado el error.

Pero lo que sí lo ha solucionado es usar el servicio $ timeout (no olvide insertarlo primero).

Por lo tanto, algo así como:

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

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

esta

quizás porque está dentro del controlador de una directiva de fábrica o simplemente necesita algún tipo de enlace, entonces harías 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();

  }]
)

31
2017-09-03 00:15