Romain Maneschi Artisan développeur
Sep 2014

$scope.$disgest vs $scope.$apply

$scope.$disgest vs $scope.$apply screenshot

Lorsqu'on veut aller plus loin avec AngularJs, on doit comprendre les mécanismes internes de ce framework. L'un des plus important est la façon dont il gère le rafraîchissement de la vue par rapport au modèle et le contraire.

A première vue, AngularJs paraît complexe, mais en réalité il est très simple de comprendre comment il fait pour maintenir à jour la vue et le modèle. A chaque fois que vous faites un {{ data }} dans le dom AngularJs fait un $scope.$watch('data', functionCallback). C'est tout simplement un écouteur sur la propriété data du $scope. Donc lorsqu'il y aura une modification de $scope.data AngularJs appellera functionCallback qui mettra à jour le dom html avec la nouvelle valeur.

La question suivante est donc, quand et comment AngularJs va détecter une modification de $scope.data ?

Imaginons un premier cas simple où l'on va mettre à jour cette donnée après 1 seconde grâce à la fonction setTimeout de javascript :

myCtrl.js
angular.module('app').controller('myCtrl', [
    '$scope',
    function($scope) {
        $scope.data = 1;
        setTimeout(function() {
            $scope.data = 2;
        }, 1000);
    }
]);

Si vous exécutez ce code il ne se passera rien. En effet, $scope.data = 2 est en dehors d'AngularJs il ne saura donc pas que le $scope a changé et ne mettra pas à jour la vue. Après une petite recherche sur le net on nous apprend qu'il faut un $scope.$apply() après avoir mis à jour $scope.data pour qu'AngularJs lance une phase de digestion.

myCtrl.js
angular.module('app').controller('myCtrl', [
    '$scope',
    function($scope) {
        $scope.data = 1;
        setTimeout(function() {
            $scope.data = 2;
            $scope.$apply();
        }, 1000);
    }
]);

Mais qu'est-ce qu'une phase de digestion ? C'est tout simplement une visite de tous les scopes de l'application, si il y a une modification sur une propriété du $scope on appelle les functionCallback des $watch (rappelez vous on en a parlé juste au dessus). On peut simplifier le code du scope comme ça :

same-as-scope.js
var watchers = [/*{data: [callbakcs]}*/];

$scope.watch = function(data, callback) {
    watchers[data] = watchers[data] || [];
    watchers[data].push(callback);
};

$scope.visit = function() {
    for(var data in watchers) {
        if(isModified(data)) {
            watchers.forEach(function(callback) {
                callback();
            });
        }
    }
    childrenScope.forEach(function(childScope) {
        childScope.visit();
    });
}

Heu, je pige pas pourquoi on fait un $scope.$apply() alors qu'on pourrait faire un $scope.$digest(), surtout que ce dernier ressemble furieusement à la digestion !?

On comprend très rapidement en allant voir l'implémentation de $scope.$apply(), il pourrait être simplifié en :

same-as-rootscope.js
$rootScope.$digest = function() {
    childrenScope.forEach(function(childScope) {
        childScope.visit();
    });
}

$scope.$apply = function() {
    $rootScope.$digest();    
};

Donc lorsqu'on appelle $scope.$apply() en réalité on lance une phase de digestion sur le $rootScope, qui est le père de tous les scopes, et va donc visiter tous les scopes de votre application.

Petite rectification, depuis le début je vous dis qu'on visite les scopes (pour suivre le pattern visitor bien évidemment), en réalité on ne visite pas un scope on le digère. En effet, la fonction visit est la fonction $digest.

Mais alors doit-on appeler $scope.$apply() ou $scope.digest() ? Si on suit les recommandations d'AngularJs il faut uniquement appeler $scope.$apply(), en effet si une modification de $scope génère une modification d'un autre $scope plus haut dans la hiérarchie de scopes et donc du dom html, nos modifications ne seront pas répercutées.

Par contre si vous savez exactement ce que vous faites un $scope.$digest() sera forcément plus performant qu'un $scope.$apply(). Mais n'oubliez pas que c'est de l'ordre de la micro-optimisation.

Exemple concret

Je ne sais pas pour vous mais moi j'aime bien valider ce qu'on m'explique. Pour réaliser cet article j'ai donc créé deux petites applications AngularJs. La première va émettre un $scope.$digest() lors d'une modification. La deuxième un $scope.$apply(). Testez par vous même en créant des scopes et en modifiant la donnée (qui n'est rien d'autre qu'un compteur s'incrémentant).

La même application mais avec $scope.apply() au lieu de $scope.$digest().

N'hésitez pas à aller voir le code, à le modifier et à jouer avec tout ça !

Pour aller plus loin

Dans AngularJs qu'est ce qui appelle $apply pour nous ?

On ne va pas se le cacher, la grande force d'AngularJs est sa magie qui fait instantanément refléter l'update de notre modèle sur notre vue et vice versa. Mais comme nous venons de le voir cette magie est en réalité un simple visitor pattern câblé avec un observer/observator pattern.

Pour que la magie opère, AngularJs réalise un $rootScope.$digest() à notre place dans les composants suivant :

Comme vous l'avez compris, il n'y a quasiment jamais besoin d'appeler explicitement $digest() ou $apply(), mais dans les rares cas où c'est nécessaire, je ne saurais que trop vous conseiller d'utiliser $scope.$evalAsync(). Cette fonction qui ne fait rien d'autre que d'attendre la fin de la digestion courante (s'il y en a une), et lancer un $rootScope.$digest().