inprog - next tick angularjs




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

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?


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.

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

Prueba esto:

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

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í!


El ciclo de digestión es una llamada síncrona. No cederá el control del bucle de eventos del navegador hasta que esté terminado. Hay algunas maneras de lidiar con esto. La forma más fácil de lidiar con esto es usar el tiempo de espera de $ construido, y una segunda forma es si está usando el guión bajo o el valor de mayúscula (y debería estarlo), llame a lo siguiente:

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

o si tiene subrayado:

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

Probamos varias soluciones alternativas y odiamos inyectar $ rootScope en todos nuestros controladores, directivas e incluso en algunas fábricas. Entonces, $ timeout y _.defer han sido nuestros favoritos hasta ahora. Estos métodos le dicen a angular que espere hasta el próximo ciclo de animación, lo que garantizará que se termine el alcance actual. $ Apply.


En primer lugar, no lo arregles de esta manera

if ( ! $scope.$$phase) { 
  $scope.$apply(); 
}

No tiene sentido porque $ phase es solo una bandera booleana para el ciclo $ digest, por lo que su $ apply () a veces no se ejecuta. Y recuerda que es una mala práctica.

En su lugar, usa $timeout

    $timeout(function(){ 
  // Any code in here will automatically have an $scope.apply() run afterwards 
$scope.myvar = newValue; 
  // And it just works! 
});

Si está usando el guión bajo o el dato, puede usar defer ():

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

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 .


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']);
};

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);
  }
}

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.


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);

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 )


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...
});

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();

  }]
)

intenta usar

$scope.applyAsync(function() {
    // your code
});

en lugar de

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

$ applyAsync Programe la invocación de $ apply para que ocurra más adelante. Esto se puede usar para poner en cola varias expresiones que deben evaluarse en el mismo resumen.

NOTA: Dentro del $ digest, $ applyAsync () solo se vaciará si el alcance actual es $ rootScope. Esto significa que si llama a $ digest en un ámbito secundario, no vaciará implícitamente la cola $ applyAsync ().

Ejemplo:

  $scope.$applyAsync(function () {
                if (!authService.authenticated) {
                    return;
                }

                if (vm.file !== null) {
                    loadService.setState(SignWizardStates.SIGN);
                } else {
                    loadService.setState(SignWizardStates.UPLOAD_FILE);
                }
            });

Referencias:

1. Scope. $ ApplyAsync () vs. Scope. $ EvalAsync () en AngularJS 1.3

  1. AngularJs Docs

similar a las respuestas anteriores, pero esto me ha funcionado fielmente ... en un servicio agrega:

    //sometimes you need to refresh scope, use this to prevent conflict
    this.applyAsNeeded = function (scope) {
        if (!scope.$$phase) {
            scope.$apply();
        }
    };

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);






angular-digest