javascript data - Comment fonctionne la liaison de données dans AngularJS?




binding bindings (12)

Je me suis demandé cela moi-même pendant un moment. Sans setters, comment AngularJS remarque-t-il des changements dans l'objet $scope ? Est-ce qu'ils les sondent?

Ce qu'il fait réellement est ceci: Tout endroit «normal» que vous modifiez le modèle a déjà été appelé depuis les tripes d'AngularJS, ainsi il appelle automatiquement $apply pour vous après l'exécution de votre code. Supposons que votre contrôleur dispose d'une méthode permettant de ng-click sur un élément. Parce que AngularJS enchaîne l'appel de cette méthode pour vous, il a une chance de faire un $apply à l'endroit approprié. De même, pour les expressions qui apparaissent directement dans les vues, celles-ci sont exécutées par AngularJS, ainsi le $apply .

Lorsque la documentation parle de devoir appeler $apply manuellement pour du code en dehors d'AngularJS , il s'agit d'un code qui, lorsqu'il est exécuté, ne provient pas d'AngularJS lui-même dans la pile des appels.

Comment fonctionne la liaison de données dans le cadre AngularJS ?

Je n'ai pas trouvé de détails techniques sur leur site . Il est plus ou moins clair comment cela fonctionne quand les données sont propagées d'une vue à l'autre. Mais comment AngularJS suit-il les changements de propriétés du modèle sans setters et getters?

J'ai trouvé qu'il y a des observateurs de JavaScript qui peuvent faire ce travail. Mais ils ne sont pas pris en charge dans Internet Explorer 6 et Internet Explorer 7 . Alors, comment AngularJS sait-il que j'ai changé par exemple ce qui suit et reflété ce changement sur une vue?

myobject.myproperty="new value";

Misko a déjà donné une excellente description du fonctionnement des liaisons de données, mais je voudrais ajouter mon point de vue sur le problème de performance avec la liaison de données.

Comme Misko l'a déclaré, environ 2000 liaisons sont là où vous commencez à voir des problèmes, mais vous ne devriez pas avoir plus de 2000 informations sur une page de toute façon. Cela peut être vrai, mais toutes les liaisons de données ne sont pas visibles pour l'utilisateur. Une fois que vous commencez à construire n'importe quel type de widget ou de grille de données avec une liaison bidirectionnelle, vous pouvez facilement atteindre 2000 liaisons, sans avoir un mauvais ux.

Considérons, par exemple, une liste déroulante où vous pouvez taper du texte pour filtrer les options disponibles. Ce type de contrôle pourrait contenir environ 150 objets et être toujours très utilisable. S'il a une fonctionnalité supplémentaire (par exemple une classe spécifique sur l'option actuellement sélectionnée), vous commencez à obtenir 3-5 liaisons par option. Mettez trois de ces widgets sur une page (par exemple un pour sélectionner un pays, l'autre pour sélectionner une ville dans ce pays, et le troisième pour sélectionner un hôtel) et vous êtes déjà entre 1000 et 2000 liaisons.

Ou envisagez une grille de données dans une application Web d'entreprise. 50 lignes par page n'est pas déraisonnable, chacune pouvant avoir 10-20 colonnes. Si vous construisez ceci avec ng-repeats, et / ou avez des informations dans certaines cellules qui utilisent des bindings, vous pourriez approcher des bindings 2000 avec cette seule grille.

Je trouve que c'est un gros problème quand on travaille avec AngularJS, et la seule solution que j'ai pu trouver jusqu'ici est de construire des widgets sans utiliser de liaison bidirectionnelle, en utilisant ngOnce, en désenregistrant des watchers et des trucs similaires, ou en construisant des directives qui construit le DOM avec jQuery et la manipulation DOM. Je pense que cela va à l'encontre de l'objectif d'utiliser Angular en premier lieu.

J'aimerais entendre des suggestions sur d'autres façons de gérer cela, mais alors peut-être que je devrais écrire ma propre question. Je voulais mettre cela dans un commentaire, mais il s'est avéré que c'était trop long pour ça ...

TL; DR
La liaison de données peut entraîner des problèmes de performances sur des pages complexes.


Angular.js crée un observateur pour chaque modèle que nous créons en vue. Chaque fois qu'un modèle est modifié, une classe "ng-dirty" est apposée sur le modèle, de sorte que l'observateur observe tous les modèles qui ont la classe "ng-dirty" et met à jour leurs valeurs dans le contrôleur et vice versa.


  1. La liaison de données unidirectionnelle est une approche dans laquelle une valeur est extraite du modèle de données et insérée dans un élément HTML. Il n'y a aucun moyen de mettre à jour le modèle de la vue. Il est utilisé dans les systèmes de gabarits classiques. Ces systèmes lient les données dans une seule direction.

  2. La liaison de données dans les applications Angular est la synchronisation automatique des données entre les composants du modèle et de la vue.

La liaison de données vous permet de traiter le modèle comme unique source de vérité dans votre application. La vue est une projection du modèle à tout moment. Si le modèle est modifié, la vue reflète le changement et vice versa.


AngularJS mémorise la valeur et la compare à une valeur précédente. C'est une vérification de base. S'il y a un changement de valeur, alors il déclenche l'événement de changement.

La méthode $apply() , que vous appelez lorsque vous passez d'un monde non-AngularJS à un monde AngularJS, appelle $digest() . Un résumé est tout simplement vieux sale-vérification. Il fonctionne sur tous les navigateurs et est totalement prévisible.

Pour contraster les auditeurs de corruption (AngularJS) et de changement ( KnockoutJS et Backbone.js ): Bien que le "dirty-check" puisse sembler simple, et même inefficace (j'aborderai cela plus tard), il s'avère qu'il est sémantiquement correct tout le temps, tandis que les auditeurs de changement ont beaucoup de cas bizarres et ont besoin de choses comme le suivi des dépendances pour le rendre plus sémantiquement correct. Le suivi des dépendances KnockoutJS est une fonction astucieuse pour un problème que AngularJS n'a pas.

Problèmes avec les auditeurs de changement:

  • La syntaxe est atroce, car les navigateurs ne la supportent pas nativement. Oui, il y a des proxies, mais ils ne sont pas sémantiquement corrects dans tous les cas, et bien sûr il n'y a pas de proxy sur les anciens navigateurs. L'essentiel est que dirty-checking vous permet de faire du POJO , alors que KnockoutJS et Backbone.js vous forcent à hériter de leurs classes, et accèdent à vos données via des accesseurs.
  • Changer la coalescence. Supposons que vous avez un tableau d'éléments. Supposons que vous souhaitiez ajouter des éléments dans un tableau, comme vous ajoutez des éléments en boucle, chaque fois que vous ajoutez des événements en cours de modification, c'est-à-dire le rendu de l'interface utilisateur. C'est très mauvais pour la performance. Ce que vous voulez, c'est mettre à jour l'interface une seule fois, à la fin. Les événements de changement sont trop fins.
  • Les changements d'écouteurs se déclenchent immédiatement sur un setter, ce qui pose un problème, car l'écouteur change change les données, ce qui déclenche un plus grand nombre d'événements de changement. C'est mauvais car sur votre pile vous pouvez avoir plusieurs événements de changement se produisant à la fois. Supposons que vous ayez deux tableaux qui doivent être synchronisés pour une raison quelconque. Vous pouvez seulement ajouter à l'un ou l'autre, mais chaque fois que vous ajoutez vous déclenchez un événement de changement, qui a maintenant une vision incohérente du monde. C'est un problème très similaire au verrouillage des threads, que JavaScript évite puisque chaque callback s'exécute exclusivement et à l'achèvement. Les événements de changement cassent ceci puisque les setters peuvent avoir des conséquences profondes qui ne sont pas voulues et non évidentes, ce qui crée à nouveau le problème du thread. Il s'avère que ce que vous voulez faire est de retarder l'exécution de l'écouteur, et garantir qu'un seul écouteur s'exécute à la fois, donc tout code est libre de changer les données, et il sait qu'aucun autre code ne s'exécute alors qu'il le fait .

Qu'en est-il des performances?

Donc, il peut sembler que nous sommes lents, puisque la vérification de la saleté est inefficace. C'est là que nous devons regarder des nombres réels plutôt que de simplement avoir des arguments théoriques, mais d'abord nous allons définir quelques contraintes.

Les humains sont:

  • Lent - Tout ce qui dépasse 50 ms est imperceptible aux humains et peut donc être considéré comme «instantané».

  • Limité - Vous ne pouvez pas vraiment montrer plus de 2000 informations à un humain sur une seule page. Quelque chose de plus que cela est une interface utilisateur vraiment mauvaise, et les humains ne peuvent pas traiter cela de toute façon.

Donc la vraie question est la suivante: Combien de comparaisons pouvez-vous faire sur un navigateur en 50 ms? C'est une question difficile à répondre car de nombreux facteurs entrent en jeu, mais voici un cas de test: http://jsperf.com/angularjs-digest/6 qui crée 10 000 observateurs. Sur un navigateur moderne, cela prend un peu moins de 6 ms. Sur Internet Explorer 8 cela prend environ 40 ms. Comme vous pouvez le voir, ce n'est pas un problème même sur les navigateurs lents ces jours-ci. Il y a une mise en garde: les comparaisons doivent être simples pour tenir dans la limite de temps ... Malheureusement, il est trop facile d'ajouter une comparaison lente dans AngularJS, il est donc facile de construire des applications lentes lorsque vous ne savez pas ce que vous faites. Mais nous espérons avoir une réponse en fournissant un module d'instrumentation, qui vous montrerait quelles sont les comparaisons lentes.

Il s'avère que les jeux vidéo et les GPUs utilisent l'approche du dirty-checking, notamment parce qu'elle est cohérente. Tant qu'ils dépassent le taux de rafraîchissement du moniteur (généralement 50-60 Hz, ou toutes les 16,6-20 ms), toute performance est un gaspillage, il est donc préférable de tirer plus de choses que d'augmenter le FPS.


Voici un exemple de liaison de données avec AngularJS, en utilisant un champ de saisie. Je t'expliquerai plus tard

Code HTML

<div ng-app="myApp" ng-controller="myCtrl" class="formInput">
     <input type="text" ng-model="watchInput" Placeholder="type something"/>
     <p>{{watchInput}}</p> 
</div>

AngularJS Code

myApp = angular.module ("myApp", []);
myApp.controller("myCtrl", ["$scope", function($scope){
  //Your Controller code goes here
}]);

Comme vous pouvez le voir dans l'exemple ci-dessus, AngularJS utilise ng-model pour écouter et observer ce qui se passe sur les éléments HTML, en particulier sur input champs de input . Quand quelque chose arrive, fais quelque chose. Dans notre cas, ng-model est lié à notre vue, en utilisant la notation moustache {{}} . Tout ce qui est tapé dans le champ de saisie s'affiche instantanément sur l'écran. Et c'est la beauté de la liaison de données, en utilisant AngularJS dans sa forme la plus simple.

J'espère que cela t'aides.

Voir un exemple de travail ici sur Codepen


Évidemment, il n'y a pas de vérification périodique de Scope s'il y a des changements dans les objets qui y sont attachés. Tous les objets attachés à la portée ne sont pas surveillés. Scope prototypiquement maintient un $$ observateurs . Scope ne fait qu'émerger à travers ce $$watchers lorsque $digest est appelé.

Angular ajoute un observateur aux observateurs de $$ pour chacun d'eux

  1. {{expression}} - Dans vos modèles (et partout ailleurs où il y a une expression) ou quand nous définissons ng-model.
  2. $ scope. $ watch ('expression / function') - Dans votre JavaScript, nous pouvons simplement attacher un objet scope pour angulaire à regarder.

La fonction $ watch prend en compte trois paramètres:

  1. Le premier est une fonction observateur qui retourne simplement l'objet ou nous pouvons simplement ajouter une expression.

  2. La seconde est une fonction d'écoute qui sera appelée quand il y a un changement dans l'objet. Toutes les choses comme les changements DOM seront implémentées dans cette fonction.

  3. Le troisième étant un paramètre optionnel qui prend un booléen. Si son vrai, profond angulaire observe l'objet & si son faux Angulaire fait juste une référence en regardant sur l'objet. La mise en œuvre approximative de $ watch ressemble à ceci

Scope.prototype.$watch = function(watchFn, listenerFn) {
   var watcher = {
       watchFn: watchFn,
       listenerFn: listenerFn || function() { },
       last: initWatchVal  // initWatchVal is typically undefined
   };
   this.$$watchers.push(watcher); // pushing the Watcher Object to Watchers  
};

Il y a une chose intéressante dans Angular appelée Digest Cycle. Le cycle $ digest commence à la suite d'un appel à $ scope. $ Digest (). Supposons que vous modifiez un modèle $ scope dans une fonction de gestionnaire via la directive ng-click. Dans ce cas, AngularJS déclenche automatiquement un cycle $ digest en appelant $ digest (). En plus de ng-click, il existe plusieurs autres directives / services intégrés qui vous permettent de changer de modèle (par exemple ng-model, $ timeout, etc.) et déclenche automatiquement un cycle $ digest. L'implémentation approximative de $ digest ressemble à ceci.

Scope.prototype.$digest = function() {
      var dirty;
      do {
          dirty = this.$$digestOnce();
      } while (dirty);
}
Scope.prototype.$$digestOnce = function() {
   var self = this;
   var newValue, oldValue, dirty;
   _.forEach(this.$$watchers, function(watcher) {
          newValue = watcher.watchFn(self);
          oldValue = watcher.last;   // It just remembers the last value for dirty checking
          if (newValue !== oldValue) { //Dirty checking of References 
   // For Deep checking the object , code of Value     
   // based checking of Object should be implemented here
             watcher.last = newValue;
             watcher.listenerFn(newValue,
                  (oldValue === initWatchVal ? newValue : oldValue),
                   self);
          dirty = true;
          }
     });
   return dirty;
 };

Si nous utilisons la fonction setTimeout () de JavaScript pour mettre à jour un modèle de portée, Angular n'a aucun moyen de savoir ce que vous pourriez changer. Dans ce cas, il est de notre responsabilité d'appeler $ apply () manuellement, ce qui déclenche un cycle $ digest. De même, si vous avez une directive qui configure un écouteur d'événement DOM et modifie certains modèles dans la fonction de gestionnaire, vous devez appeler $ apply () pour vous assurer que les modifications prennent effet. La grande idée de $ apply est que nous pouvons exécuter du code qui ne connaît pas Angular, ce code peut encore changer les choses sur la portée. Si nous appliquons ce code dans $ apply, il se chargera d'appeler $ digest (). Implémentation approximative de $ apply ().

Scope.prototype.$apply = function(expr) {
       try {
         return this.$eval(expr); //Evaluating code in the context of Scope
       } finally {
         this.$digest();
       }
};

C'est ma compréhension de base. C'est peut-être faux!

  1. Les éléments sont regardés en passant une fonction (renvoyant la chose à surveiller) à la méthode $watch .
  2. Les modifications apportées aux éléments surveillés doivent être effectuées dans un bloc de code enveloppé par la méthode $apply .
  3. A la fin de $apply la méthode $digest est invoquée. Elle passe par chacune des montres et vérifie si elles ont changé depuis la dernière exécution du $digest .
  4. Si des modifications sont trouvées, le résumé est invoqué à nouveau jusqu'à ce que toutes les modifications se stabilisent.

En développement normal, la syntaxe de liaison de données dans le code HTML indique au compilateur AngularJS de créer les montres pour vous et les méthodes de contrôleur sont exécutées dans $apply déjà. Donc, pour le développeur de l'application, tout est transparent.


En vérifiant l'objet $scope

Angular maintient un simple array d'observateurs dans les objets $scope . Si vous inspectez un $scope vous constaterez qu'il contient un array appelé $$watchers .

Chaque observateur est un object qui contient entre autres choses

  1. Une expression que l'observateur surveille. Cela peut simplement être un nom d' attribute , ou quelque chose de plus compliqué.
  2. Une dernière valeur connue de l'expression. Cela peut être vérifié par rapport à la valeur calculée actuelle de l'expression. Si les valeurs diffèrent, l'observateur déclenchera la fonction et marquera $scope comme sale.
  3. Une fonction qui sera exécutée si l'observateur est sale.

Comment les observateurs sont définis

Il y a plusieurs façons de définir un observateur dans AngularJS.

  • Vous pouvez explicitement $watch un attribute sur $scope .

    $scope.$watch('person.username', validateUnique);
    
  • Vous pouvez placer une interpolation {{}} dans votre modèle (un observateur sera créé pour vous sur la $scope actuelle).

    <p>username: {{person.username}}</p>
    
  • Vous pouvez demander une directive telle que ng-model pour définir l'observateur pour vous.

    <input ng-model="person.username" />
    

Le cycle $digest vérifie tous les observateurs par rapport à leur dernière valeur

Lorsque nous interagissons avec AngularJS via les canaux normaux (modèle ng, ng-repeat, etc.), un cycle de digestion est déclenché par la directive.

Un cycle de digest est une traversée en profondeur de $scope et de tous ses enfants . Pour chaque object $scope , nous parcourons son array $$watchers et évaluons toutes les expressions. Si la nouvelle valeur d'expression est différente de la dernière valeur connue, la fonction de l'observateur est appelée. Cette fonction peut recompiler une partie du DOM, recalculer une valeur sur $scope , déclencher une request AJAX , tout ce dont vous avez besoin.

Chaque portée est traversée et chaque expression de surveillance est évaluée et vérifiée par rapport à la dernière valeur.

Si un observateur est déclenché, la $scope est sale

Si un observateur est déclenché, l'application sait que quelque chose a changé et le $scope est marqué comme sale.

Les fonctions d'observateur peuvent modifier d'autres attributs sur $scope ou sur une $scope parent. Si une fonction $watcher a été déclenchée, nous ne pouvons pas garantir que nos autres $scope sont toujours propres, et nous exécutons à nouveau le cycle entier de digestion.

C'est parce que AngularJS a une liaison bidirectionnelle, donc les données peuvent être retransmises dans l'arborescence $scope . Nous pouvons changer une valeur sur une $scope plus élevée qui a déjà été digérée. Peut-être que nous changeons une valeur sur $rootScope .

Si le $digest est sale, nous $digest nouveau le cycle $digest entier

Nous bouclons continuellement le cycle $digest jusqu'à ce que le cycle digest soit propre (toutes $watch expressions $watch ont la même valeur que dans le cycle précédent), ou nous atteignons la limite digest. Par défaut, cette limite est fixée à 10.

Si nous atteignons la limite digest AngularJS va déclencher une erreur dans la console:

10 $digest() iterations reached. Aborting!

Le condensé est dur sur la machine mais facile sur le développeur

Comme vous pouvez le voir, chaque fois que quelque chose change dans une application AngularJS, AngularJS vérifie chaque observateur dans la hiérarchie $scope pour voir comment répondre. Pour un développeur, c'est une aubaine pour la productivité, car vous n'avez plus besoin d'écrire de code de câblage, AngularJS remarquera simplement si une valeur a changé et rendra le reste de l'application compatible avec le changement.

Du point de vue de la machine bien que ce soit follement inefficace et ralentira notre application si nous créons trop d'observateurs. Misko a cité un chiffre d'environ 4000 observateurs avant que votre application se sentira lente sur les anciens navigateurs.

Cette limite est facile à atteindre si vous ng-repeat sur un grand array JSON par exemple. Vous pouvez atténuer cela en utilisant des fonctionnalités telles que la liaison unique pour compiler un modèle sans créer de surveillance.

Comment éviter de créer trop de watchers

Chaque fois que votre utilisateur interagit avec votre application, chaque observateur de votre application sera évalué au moins une fois. Une grande partie de l'optimisation d'une application AngularJS consiste à réduire le nombre d'observateurs dans votre arborescence $scope . Un moyen facile de le faire est avec une liaison de temps .

Si vous avez des données qui vont rarement changer, vous pouvez les lier une seule fois en utilisant la syntaxe ::, comme ceci:

<p>{{::person.username}}</p>

ou

<p ng-bind="::person.username"></p>

La liaison ne sera déclenchée que lorsque le modèle contenant sera rendu et que les données seront chargées dans $scope .

Ceci est particulièrement important lorsque vous avez une ng-repeat avec de nombreux éléments.

<div ng-repeat="person in people track by username">
  {{::person.username}}
</div>

Il est arrivé que je devais lier un modèle de données d'une personne avec un formulaire, ce que j'ai fait était une cartographie directe des données avec le formulaire.

Par exemple si le modèle avait quelque chose comme:

$scope.model.people.name

L'entrée de contrôle du formulaire:

<input type="text" name="namePeople" model="model.people.name">

De cette façon, si vous modifiez la valeur du contrôleur d'objet, cela sera automatiquement reflété dans la vue.

Un exemple où j'ai passé le modèle est mis à jour à partir des données du serveur lorsque vous demandez un code postal et un code postal basé sur des charges écrites une liste de colonies et villes associées à cette vue, et définissez par défaut la première valeur avec l'utilisateur. Et cela j'ai très bien fonctionné, ce qui arrive, c'est que angularJS prend parfois quelques secondes pour rafraichir le modèle, pour ce faire vous pouvez mettre un spinner en affichant les données.


Expliquer avec des images:

Data-Binding a besoin d'une cartographie

La référence dans la portée n'est pas exactement la référence dans le modèle. Lorsque vous liez deux objets, vous avez besoin d'un troisième qui écoute le premier et modifie l'autre.

Ici, lorsque vous modifiez le <input> , vous touchez le data-ref3 . Et le mécanisme classique de liaison de données va changer data-ref4 . Alors, comment vont se déplacer les autres {{data}} expressions?

Les événements mènent à $ digest ()

Angulaire conserve une oldValue et une oldValue newValue de chaque liaison. Et après chaque événement Angular , la fameuse boucle $digest() va vérifier la WatchList pour voir si quelque chose a changé. Ces événements angulaires sont ng-click , ng-change , $http complétés ... Le $digest() boucle tant que oldValue diffère de newValue .

Dans l'image précédente, il remarquera que data-ref1 et data-ref2 ont changé.

Conclusions

C'est un peu comme l'oeuf et le poulet. Vous ne savez jamais qui commence, mais j'espère que cela fonctionne la plupart du temps comme prévu.

L'autre point est que vous pouvez comprendre facilement l'impact profond d'une simple liaison sur la mémoire et le CPU. Espérons que les ordinateurs de bureau sont assez gros pour gérer cela. Les téléphones mobiles ne sont pas si forts.


Javascript:

window.location.href='www.your_url.com';
window.top.location.href='www.your_url.com';
window.location.replace('www.your_url.com');

Jquery:

var url='www.your_url.com';
$(location).attr('href',url);
$(location).prop('href',url);//instead of location you can use window






javascript angularjs data-binding