Pourquoi utiliser if (! $ Scope. $$ phase) $ scope. $ Apply () est un anti-pattern?

92

Parfois, j'ai besoin d'utiliser $scope.$applydans mon code et parfois cela génère une erreur "Digest déjà en cours". J'ai donc commencé à trouver un moyen de contourner cela et j'ai trouvé cette question: AngularJS: Empêche l'erreur $ digest déjà en cours lors de l'appel de $ scope. $ Apply () . Cependant, dans les commentaires (et sur le wiki angulaire), vous pouvez lire:

Ne faites pas si (! $ Scope. $$ phase) $ scope. $ Apply (), cela signifie que votre $ scope. $ Apply () n'est pas assez haut dans la pile d'appels.

Alors maintenant, j'ai deux questions:

  1. Pourquoi exactement est-ce un anti-modèle?
  2. Comment puis-je utiliser $ scope. $ Apply en toute sécurité?

Une autre «solution» pour éviter l'erreur «digest déjà en cours» semble utiliser $ timeout:

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

Est-ce la voie à suivre? Est-ce plus sûr? Voici donc la vraie question: comment puis-je éliminer entièrement la possibilité d'une erreur "digest déjà en cours"?

PS: J'utilise uniquement $ scope. $ Apply dans des callbacks non angularjs qui ne sont pas synchrones. (pour autant que je sache, ce sont des situations dans lesquelles vous devez utiliser $ scope. $ apply si vous voulez que vos modifications soient appliquées)

Dominik Goltermann
la source
D'après mon expérience, vous devriez toujours savoir si vous manipulez scopede l'intérieur angulaire ou de l'extérieur angulaire. Donc, selon cela, vous savez toujours si vous devez appeler scope.$applyou non. Et si vous utilisez le même code pour la scopemanipulation angulaire / non angulaire , vous le faites mal, il devrait toujours être séparé ... donc fondamentalement, si vous rencontrez un cas où vous devez vérifier scope.$$phase, votre code n'est pas conçu de manière correcte, et il y a toujours un moyen de le faire `` de la bonne manière ''
doodeec
1
Je n'utilise cela que dans les rappels non angulaires (!) C'est pourquoi je suis confus
Dominik Goltermann
2
s'il n'était pas angulaire, il ne lancerait pas d' digest already in progresserreur
doodeec
1
c'est ce que je pensais. Le fait est que cela ne lance pas toujours l'erreur. Seulement de temps en temps. Je soupçonne que l'appliquer entre PAR CHANCE avec un autre condensé. Est-ce possible?
Dominik Goltermann
Je ne pense pas que ce soit possible si le rappel est strictement non angulaire
doodeec

Réponses:

113

Après quelques recherches supplémentaires, j'ai pu résoudre la question de savoir si son utilisation est toujours sûre $scope.$apply. La reponse courte est oui.

Longue réponse:

En raison de la façon dont votre navigateur exécute Javascript, il n'est pas possible que deux appels de résumé se heurtent par hasard .

Le code JavaScript que nous écrivons ne s'exécute pas tous en une seule fois, il s'exécute à tour de rôle. Chacun de ces tours s'exécute sans interruption du début à la fin, et lorsqu'un tour est en cours, rien d'autre ne se passe dans notre navigateur. (à partir de http://jimhoskins.com/2012/12/17/angularjs-and-apply.html )

Par conséquent, l'erreur "digest déjà en cours" ne peut se produire que dans une situation: lorsqu'un $ apply est émis dans un autre $ apply, par exemple:

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

Cette situation ne peut pas se produire si nous utilisons $ scope.apply dans un callback pur non angularjs, comme par exemple le callback de setTimeout. Le code suivant est donc à 100% à l'épreuve des balles et il n'est pas nécessaire de faire unif (!$scope.$$phase) $scope.$apply()

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

même celui-ci est sûr:

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

Ce qui n'est PAS sûr (car $ timeout - comme tous les helpers angularjs - $scope.$applyvous appelle déjà ):

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

Cela explique également pourquoi l'utilisation de if (!$scope.$$phase) $scope.$apply()est un anti-pattern. Vous n'en avez tout simplement pas besoin si vous l'utilisez $scope.$applyde la bonne manière: dans un callback pur js comme setTimeoutpar exemple.

Lisez http://jimhoskins.com/2012/12/17/angularjs-and-apply.html pour une explication plus détaillée.

Dominik Goltermann
la source
J'ai eu un exemple où je crée un service avec $document.bind('keydown', function(e) { $rootScope.$apply(function() { // a passed through function from the controller gets executed here }); });Je ne sais vraiment pas pourquoi je dois faire $ apply ici, car j'utilise $ document.bind ..
Betty St
car $ document n'est qu'un «wrapper jQuery ou jqLite pour l'objet window.document du navigateur». et mis en œuvre comme suit: function $DocumentProvider(){ this.$get = ['$window', function(window){ return jqLite(window.document); }]; }Il n'y a pas d'application ici.
Dominik Goltermann
11
$timeoutsignifie sémantiquement exécuter le code après un délai. Cela pourrait être une chose fonctionnellement sûre à faire, mais c'est un hack. Il devrait y avoir un moyen sûr d'utiliser $ apply lorsque vous ne pouvez pas savoir si un $digestcycle est en cours ou si vous êtes déjà dans un fichier $apply.
John Strickler
1
une autre raison pour laquelle c'est mauvais: il utilise des variables internes (phase $$) qui ne font pas partie de l'API publique et elles pourraient être modifiées dans une version plus récente d'angular et ainsi casser votre code. Votre problème avec le déclenchement d'événements synchrones est intéressant cependant
Dominik Goltermann
4
Une approche plus récente consiste à utiliser $ scope. $ EvalAsync () qui s'exécute en toute sécurité dans le cycle de résumé actuel si possible ou dans le cycle suivant. Reportez-vous à bennadel.com/blog
jaymjarri
16

C'est très certainement un anti-pattern maintenant. J'ai vu un résumé exploser même si vous vérifiez la phase $$. Vous n'êtes tout simplement pas censé accéder à l'API interne indiquée par des $$préfixes.

Tu devrais utiliser

 $scope.$evalAsync();

car il s'agit de la méthode préférée dans Angular ^ 1.4 et est spécifiquement exposée en tant qu'API pour la couche d'application.

FlavourScape
la source
9

Dans tous les cas lorsque votre digest en cours et que vous poussez un autre service à digérer, cela donne simplement une erreur ie digest déjà en cours. donc pour remédier à cela, vous avez deux options. vous pouvez vérifier tout autre résumé en cours, comme le sondage.

Premier

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

si la condition ci-dessus est vraie, vous pouvez appliquer votre $ scope. $ apply otherwies not and

la deuxième solution est d'utiliser $ timeout

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

il ne laissera pas l'autre condensé démarrer tant que $ timeout n'aura pas terminé son exécution.

Lalit Sachdeva
la source
1
a voté contre; La question demande spécifiquement pourquoi NE PAS faire ce que vous décrivez ici, pas pour une autre façon de contourner cela. Voir l'excellente réponse de @gaul pour savoir quand l'utiliser $scope.$apply();.
PureSpider
Sans répondre à la question: $timeoutc'est la clé! cela fonctionne et plus tard, j'ai trouvé que c'était recommandé aussi.
Himel Nag Rana
Je sais qu'il est assez tard pour ajouter un commentaire à cela 2 ans plus tard, mais soyez prudent lorsque vous utilisez trop $ timeout, car cela peut vous coûter trop cher en performances si vous n'avez pas une bonne structure d'application
cpoDesign
9

scope.$applydéclenche un $digestcycle fondamental pour la liaison de données bidirectionnelle

Un $digestcycle vérifie les objets, c'est-à-dire les modèles (pour être précis $watch) attachés pour $scopeévaluer si leurs valeurs ont changé et s'il détecte un changement, il prend les mesures nécessaires pour mettre à jour la vue.

Maintenant, lorsque vous utilisez $scope.$applyvous faites face à une erreur "Déjà en cours" donc il est assez évident qu'un $ digest est en cours d'exécution mais qu'est-ce qui l'a déclenché?

ans -> tous les $httpappels, tous les ng-clics, répéter, afficher, masquer, etc. déclenchent un $digestcycle ET LA PIRE PARTIE QUI S'EXÉCUTE DE CHAQUE $ SCOPE.

c'est-à-dire que votre page a 4 contrôleurs ou directives A, B, C, D

Si vous avez 4 $scopepropriétés dans chacune d'elles, vous avez un total de 16 propriétés de portée $ sur votre page.

Si vous déclenchez $scope.$applydans le contrôleur D, un $digestcycle vérifiera les 16 valeurs !!! plus toutes les propriétés $ rootScope.

Réponse -> mais $scope.$digestdéclenche un $digestsur enfant et même portée, il ne vérifie donc que 4 propriétés. Donc, si vous êtes sûr que les changements dans D n'affecteront pas A, B, C, utilisez $scope.$digest not $scope.$apply.

Ainsi, un simple ng-click ou ng-show / hide pourrait déclencher un $digestcycle sur plus de 100 propriétés, même si l'utilisateur n'a déclenché aucun événement !

Rishul Matta
la source
2
Ouais, je l'ai réalisé tardivement dans le projet, malheureusement. Je n'aurais pas utilisé Angular si j'avais su cela dès le début. Toutes les directives standard déclenchent un $ scope. $ Apply, qui à son tour appelle $ rootScope. $ Digest, qui effectue des vérifications sales sur TOUTES les portées. Mauvaise décision de conception si vous me demandez. Je devrais être en contrôle des oscilloscopes à vérifier, car JE SAIS COMMENT LES DONNÉES SONT LIÉES À CES SCOPES!
MoonStom
0

Utilisez $timeout, c'est la voie recommandée.

Mon scénario est que je dois modifier les éléments de la page en fonction des données que j'ai reçues d'un WebSocket. Et comme il est en dehors d'Angular, sans le $ timeout, le seul modèle sera changé mais pas la vue. Parce qu'Angular ne sait pas que cette donnée a été modifiée. $timeoutdit fondamentalement à Angular d'effectuer le changement lors de la prochaine série de $ digest.

J'ai également essayé ce qui suit et cela fonctionne. La différence pour moi est que $ timeout est plus clair.

setTimeout(function(){
    $scope.$apply(function(){
        // changes
    });
},0)
James J. Ye
la source
Il est beaucoup plus simple d'encapsuler le code de votre socket dans $ apply (un peu comme Angular sur le code AJAX, c'est-à-dire $http). Sinon, vous devez répéter ce code partout.
timruffles
ce n'est certainement pas recommandé. De plus, vous obtiendrez parfois une erreur si $ scope a $$ phase. à la place, vous devriez utiliser $ scope. $ evalAsync ();
FlavorScape
Il n'est pas nécessaire de savoir $scope.$applysi vous utilisez setTimeoutou$timeout
Kunal
-1

J'ai trouvé une solution très cool:

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

injectez là où vous en avez besoin:

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