¿Por qué usar if (! $ Scope. $$ phase) $ scope. $ Apply () es un anti-patrón?

92

A veces necesito usar $scope.$applyen mi código y, a veces, arroja un error de "resumen ya en progreso". Así que comencé a encontrar una manera de evitar esto y encontré esta pregunta: AngularJS: Prevenga el error $ digest ya en progreso al llamar a $ scope. $ Apply () . Sin embargo, en los comentarios (y en la wiki angular) puedes leer:

No lo haga si (! $ Scope. $$ phase) $ scope. $ Apply (), significa que su $ scope. $ Apply () no es lo suficientemente alto en la pila de llamadas.

Entonces ahora tengo dos preguntas:

  1. ¿Por qué exactamente es esto un anti-patrón?
  2. ¿Cómo puedo usar $ scope. $ De forma segura?

Otra "solución" para evitar el error "resumen ya en progreso" parece estar usando $ timeout:

$timeout(function() {
  //...
});

¿Es ese el camino a seguir? Es mas seguro? Entonces, aquí está la pregunta real: ¿Cómo puedo eliminar por completo la posibilidad de un error de "resumen ya en progreso"?

PD: solo estoy usando $ scope. $ Apply en devoluciones de llamada no angularjs que no son síncronas. (hasta donde yo sé, esas son situaciones en las que debe usar $ scope. $ apply si desea que se apliquen sus cambios)

Dominik Goltermann
fuente
Desde mi experiencia, siempre debe saber si está manipulando scopedesde dentro de angular o desde fuera de angular. Entonces, de acuerdo con esto, siempre sabrá si necesita llamar scope.$applyo no. Y si está usando el mismo código para scopemanipulación angular / no angular , lo está haciendo mal, siempre debe estar separado ... así que básicamente si se encuentra con un caso en el que necesita verificar scope.$$phase, su código no es diseñado de manera correcta, y siempre hay una manera de hacerlo 'de la manera correcta'
doodeec
1
Solo estoy usando esto en devoluciones de llamada no angulares (!) Por eso estoy confundido
Dominik Goltermann
2
si no fuera angular, no arrojaría un digest already in progresserror
doodeec
1
es lo que pensaba. La cuestión es que no siempre arroja el error. Solo de vez en cuando. Mi sospecha es que la aplicación colisiona por casualidad con otro resumen. ¿Es eso posible?
Dominik Goltermann
No creo que sea posible si la devolución de llamada es estrictamente no angular
doodeec

Respuestas:

113

Después de investigar un poco más, pude resolver la pregunta de si siempre es seguro de usar $scope.$apply. La respuesta corta es sí.

Respuesta larga:

Debido a la forma en que su navegador ejecuta Javascript, no es posible que dos llamadas de resumen colisionen por casualidad .

El código JavaScript que escribimos no se ejecuta de una vez, sino que se ejecuta por turnos. Cada uno de estos turnos se ejecuta sin interrupciones de principio a fin, y cuando se ejecuta un turno, no sucede nada más en nuestro navegador. (de http://jimhoskins.com/2012/12/17/angularjs-and-apply.html )

Por lo tanto, el error "resumen ya en progreso" solo puede ocurrir en una situación: cuando se emite un $ aplicar dentro de otro $ aplicar, por ejemplo:

$scope.apply(function() {
  // some code...
  $scope.apply(function() { ... });
});

Esta situación no puede surgir si usamos $ scope.apply en una devolución de llamada pura no angularjs, como por ejemplo la devolución de llamada de setTimeout. Por lo que el siguiente código es 100% a prueba de balas y no hay ninguna necesidad de hacer unaif (!$scope.$$phase) $scope.$apply()

setTimeout(function () {
    $scope.$apply(function () {
        $scope.message = "Timeout called!";
    });
}, 2000);

incluso este es seguro:

$scope.$apply(function () {
    setTimeout(function () {
        $scope.$apply(function () {
            $scope.message = "Timeout called!";
        });
    }, 2000);
});

Lo que NO es seguro (porque $ timeout, como todos los ayudantes de angularjs, ya lo llama $scope.$apply):

$timeout(function () {
    $scope.$apply(function () {
        $scope.message = "Timeout called!";
    });
}, 2000);

Esto también explica por qué el uso de if (!$scope.$$phase) $scope.$apply()es un anti-patrón. Simplemente no lo necesita si lo usa $scope.$applyde la manera correcta: en una devolución de llamada js pura como, setTimeoutpor ejemplo.

Lea http://jimhoskins.com/2012/12/17/angularjs-and-apply.html para obtener una explicación más detallada.

Dominik Goltermann
fuente
Tengo un ejemplo en el que creo un servicio con $document.bind('keydown', function(e) { $rootScope.$apply(function() { // a passed through function from the controller gets executed here }); });No sé por qué tengo que hacer que $ aplicar aquí, porque estoy usando $ document.bind ..
Betty St
porque $ document es solo "Un contenedor jQuery o jqLite para el objeto window.document del navegador". e implementado de la siguiente manera: function $DocumentProvider(){ this.$get = ['$window', function(window){ return jqLite(window.document); }]; }No hay aplicación allí.
Dominik Goltermann
11
$timeoutsemánticamente significa ejecutar código después de un retraso. Puede que sea funcionalmente seguro, pero es un truco. Debe haber una forma segura de usar $ apply cuando no pueda saber si un $digestciclo está en progreso o si ya está dentro de un $apply.
John Strickler
1
otra razón por la que es malo: utiliza variables internas (fase $$) que no son parte de la api pública y pueden cambiarse en una versión más nueva de angular y así romper su código. Sin embargo
sincrónicos
4
El enfoque más nuevo es usar $ scope. $ EvalAsync () que se ejecuta de forma segura en el ciclo de resumen actual si es posible o en el próximo ciclo. Consulte bennadel.com/blog/…
jaymjarri
16

Definitivamente ahora es un anti-patrón. He visto explotar un resumen incluso si compruebas la fase $$. No se supone que acceda a la API interna indicada por $$prefijos.

Deberías usar

 $scope.$evalAsync();

ya que este es el método preferido en Angular ^ 1.4 y se expone específicamente como una API para la capa de aplicación.

FlavorScape
fuente
9

En cualquier caso, cuando su resumen está en progreso y presiona otro servicio para que lo haga, simplemente da un error, es decir, el resumen ya está en progreso. así que para curar esto tienes dos opciones. puede comprobar si hay otro resumen en curso, como sondeos.

El primero

if ($scope.$root.$$phase != '$apply' && $scope.$root.$$phase != '$digest') {
    $scope.$apply();
}

si la condición anterior es verdadera, entonces puede aplicar su $ scope. $ aplicar de otro modo y no

la segunda solución es usar $ timeout

$timeout(function() {
  //...
})

no permitirá que el otro resumen comience hasta que $ timeout complete su ejecución.

Lalit Sachdeva
fuente
1
voto negativo La pregunta pregunta específicamente por qué NO hacer lo que está describiendo aquí, no por otra forma de esquivarlo. Vea la excelente respuesta de @gaul para saber cuándo usar $scope.$apply();.
PureSpider
Aunque no responde la pregunta: ¡ $timeoutes la clave! Funciona y luego descubrí que también se recomienda.
Himel Nag Rana
Sé que es bastante tarde para agregar comentarios a esto 2 años después, pero tenga cuidado cuando use $ timeout demasiado, ya que esto puede costarle demasiado en rendimiento si no tiene una buena estructura de aplicación
cpoDesign
9

scope.$applydesencadena un $digestciclo que es fundamental para el enlace de datos bidireccional

Un $digestciclo busca objetos, es decir, modelos (para ser precisos $watch) adjuntos para $scopeevaluar si sus valores han cambiado y si detecta un cambio, entonces toma los pasos necesarios para actualizar la vista.

Ahora, cuando usa, $scope.$applyse enfrenta a un error "Ya en progreso", por lo que es bastante obvio que se está ejecutando un $ digest, pero ¿qué lo desencadenó?

ans -> cada $httpllamada, todo ng-click, repetir, mostrar, ocultar, etc. desencadenan un $digestciclo Y LA PEOR PARTE SE EJECUTA DE CADA $ ALCANCE.

es decir, digamos que su página tiene 4 controladores o directivas A, B, C, D

Si tiene 4 $scopepropiedades en cada una de ellas, entonces tiene un total de 16 propiedades $ scope en su página.

¡Si dispara $scope.$applyen el controlador D, un $digestciclo comprobará los 16 valores! más todas las propiedades de $ rootScope.

Respuesta -> pero $scope.$digestactiva un $digesthijo y el mismo alcance, por lo que solo verificará 4 propiedades. Entonces, si está seguro de que los cambios en D no afectarán a A, B, C, use $scope.$digest not $scope.$apply.

Por lo tanto, un simple clic ng o ng-show / hide podría desencadenar un $digestciclo en más de 100 propiedades, ¡incluso cuando el usuario no ha disparado ningún evento !

Rishul Matta
fuente
2
Sí, me di cuenta tan tarde en el proyecto, desafortunadamente. No habría usado Angular si hubiera sabido esto desde el principio. Todas las directivas estándar activan un $ scope. $ Apply, que a su vez llama a $ rootScope. $ Digest, que realiza comprobaciones sucias en TODOS los ámbitos. Mala decisión de diseño si me preguntas. ¡Debería tener el control de qué osciloscopios deben ser revisados ​​sucios, porque SÉ CÓMO LOS DATOS ESTÁN VINCULADOS A ESTOS ALCANCE!
MoonStom
0

Utilizar $timeout, es la forma recomendada.

Mi escenario es que necesito cambiar elementos en la página según los datos que recibí de un WebSocket. Y como está fuera de Angular, sin $ timeout, se cambiará el único modelo, pero no la vista. Porque Angular no sabe que se ha cambiado ese dato. $timeoutbásicamente le está diciendo a Angular que haga el cambio en la siguiente ronda de $ digest.

Intenté lo siguiente también y funciona. La diferencia para mí es que $ timeout es más claro.

setTimeout(function(){
    $scope.$apply(function(){
        // changes
    });
},0)
James J. Ye
fuente
Es mucho más limpio envolver su código de socket en $ apply (al igual que Angular en el código AJAX, es decir $http). De lo contrario, debe repetir este código por todas partes.
timruffles
esto definitivamente no es recomendable. Además, ocasionalmente obtendrá un error al hacer esto si $ scope tiene $$ fase. en su lugar, debe usar $ scope. $ evalAsync ();
FlavorScape
No es necesario $scope.$applysi está utilizando setTimeouto$timeout
Kunal
-1

Encontré una solución muy buena:

.factory('safeApply', [function($rootScope) {
    return function($scope, fn) {
        var phase = $scope.$root.$$phase;
        if (phase == '$apply' || phase == '$digest') {
            if (fn) {
                $scope.$eval(fn);
            }
        } else {
            if (fn) {
                $scope.$apply(fn);
            } else {
                $scope.$apply();
            }
        }
    }
}])

inyecte eso donde lo necesite:

.controller('MyCtrl', ['$scope', 'safeApply',
    function($scope, safeApply) {
        safeApply($scope); // no function passed in
        safeApply($scope, function() { // passing a function in
        });
    }
])
bora89
fuente