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 scope 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() desde el scope 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?

No use este patrón : esto causará más errores de los que resuelve. Aunque piensas que arregló algo, no lo hizo.

Puede verificar si un $digest ya está en progreso al marcar $scope.$$phase .

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

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

Para el punto de @ dnc253, si te encuentras llamando $digest o $apply frecuencia, puedes estar haciendo mal. En general, creo que necesito digerir cuando necesito actualizar el estado del scope como resultado de un evento DOM que se dispara fuera del scope de Angular. Por ejemplo, cuando un modal de arranque de Twitter se oculta. A veces, el evento DOM se activa cuando hay un $digest en curso, 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 lo haga if (!$scope.$$phase) $scope.$apply() , significa que $scope.$apply() no es lo suficientemente alto en la stack de llamadas.

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

Aquí hay un ejemplo de un servicio. (En aras de la brevedad, el rest 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á de manera predeterminada en 0 si se deja sin configurar ( $ timeout llama a $ browser.defer, cuyo valor predeterminado es 0 si no se establece el retraso )

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 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 scope actual. $ Apply haya terminado.

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

Cosas que deberías saber

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

  • $timeout(callback) esperará hasta que finalice el ciclo de resumen actual (si lo hay), luego ejecutará la callback, luego ejecutará al final $apply completo.

  • $timeout(callback, delay, false) hará lo mismo (con un retraso opcional antes de ejecutar la callback), pero no activará $apply (tercer argumento) que guarda las actuaciones si no modificó su modelo angular ($ scope )

  • $scope.$apply(callback) invoca, entre otras cosas, $rootScope.$digest , lo que significa que volverá a compendiar el ámbito 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 scope de sus padres, lo que puede ahorrar muchas actuaciones cuando se trabaja en una parte aislada de su HTML con un scope aislado (principalmente de una directiva) . $ digest no toma una callback: 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 obtiene el $digest already in progress error , entonces su architecture está equivocada: o bien no necesita volver a subir el scope, o no debe estar a cargo de eso (ver a continuación).

Cómo estructurar tu código

Cuando obtiene ese error, está tratando de digerir su scope mientras ya está en curso: ya que no conoce el estado de su scope 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 callback dentro del ciclo de resumen actual si está ocurriendo, de lo contrario un nuevo ciclo de resumen comenzará a ejecutar la callback.

Eso todavía no es tan bueno como un $scope.$digest si realmente sabes que solo necesitas sincronizar una parte aislada de tu HTML (ya que se lanzará una nueva $apply si ninguna está en progreso), pero este es el mejor solución cuando está ejecutando una función que no puede conocer si se ejecutará de forma síncrona o no , por ejemplo después de recuperar un recurso potencialmente en caché: a veces esto requerirá una llamada asincrónica a un servidor; de lo contrario, el recurso se obtendrá de manera local sincrónicamente.

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

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

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

El problema surge cuando tiene una llamada a $apply que a veces se ejecuta asincrónicamente fuera del código angular (cuando se debe usar $ apply) y, a veces sincrónicamente dentro del código angular (lo que provoca el error $digest already in progress ).

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 ejecute de forma asíncrona. Esto puede hacerse ejecutando su código dentro de una llamada a $timeout con la demora establecida en 0 (que es la predeterminada). Sin embargo, si llama al código dentro de $timeout elimina la necesidad de llamar $apply , porque $ timeout desencadenará otro ciclo de $digest por sí solo, 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 call $apply cuando sabe 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 callback 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 over $apply , no veo por qué no siempre podría usar $timeout (con cero retraso) en lugar de $apply , ya que hará aproximadamente lo mismo.

Tuve el mismo problema con los guiones de terceros como CodeMirror, por ejemplo, y Krpano, e incluso el uso de los métodos de 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(); }] ) 

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

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

 $timeout(angular.noop) 

También puedes usar evalAsync. ¡Se ejecutará en algún momento después de que el resumen haya terminado!

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

A veces, aún recibirá errores si utiliza de esta manera ( https://stackoverflow.com/a/12859093/801426 ).

Prueba esto:

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

Antes que nada, no lo arregles de esta manera

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

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

En cambio, use $timeout

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

Si usa subrayado o lodash, puede usar defer ():

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

Debe usar $ evalAsync o $ timeout según el contexto.

Este es un enlace con una buena explicación:

http://www.bennadel.com/blog/2605-scope-evalasync-vs-timeout-in-angularjs.htm

Te aconsejaría que uses un evento personalizado en lugar de activar un ciclo de resumen.

Descubrí que la transmisión de eventos personalizados y el registro de oyentes para estos eventos es una buena solución para activar una acción que deseas que ocurra ya sea que estés 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 oyentes suscritos a dicho evento y NO dispara todos los relojes vinculados al scope como lo haría si invoca el scope. $ Apply.

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

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

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

Según los documentos , $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 scope, y un $ watch en esa variable cambia otras variables que tienen que ser $applied . Este último paso causa el error “digerir ya en progreso”.

Al reemplazar $apply con $eval dentro de la expresión watch, las variables del ámbito se actualizan como se esperaba.

Por lo tanto, parece que si el resumen se va a ejecutar de todos modos debido a algún otro cambio dentro de Angular, $eval es lo único que tiene que hacer.

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

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 en un momento posterior. Esto se puede utilizar para poner en cola varias expresiones que deben evaluarse en el mismo resumen.

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

Exmaple:

  $scope.$applyAsync(function () { if (!authService.authenticated) { return; } if (vm.file !== null) { loadService.setState(SignWizardStates.SIGN); } else { loadService.setState(SignWizardStates.UPLOAD_FILE); } }); 

Referencias

1. Ámbito de aplicación. $ ApplyAsync () frente a Scope. $ EvalAsync () en AngularJS 1.3

  1. Documentos de AngularJs

Entendiendo que los documentos angulares llaman revisando la $$phase un antipatrón , traté de obtener $timeout y _.defer para que funcionen.

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

Lo único que funciona siempre es:

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

No entiendo el peligro de este método, o por qué es descrito como un hack por 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? $ safeApply se ve bien, pero también usa el método $$phase inspección de $$phase .

Este es mi servicio utils:

 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 para 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 haya terminado y luego se apply() activadores apply() . Simplemente llame a la función apply() 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); } } 

similar a las respuestas anteriores, pero esto ha funcionado fielmente para mí … en un servicio, agregue:

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

Puedes usar

$timeout

para prevenir el error

  $timeout(function () { var scope = angular.element($("#myController")).scope(); scope.myMethod(); scope.$scope(); },1); 

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 el código de func ‘safeApply’:

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

Esto resolverá tu problema:

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