Introduction
La portée se situe dans une position centrale dans l'écosystème NG. La couche sous-jacente de la liaison bidirectionnelle revendiquée par NG au monde extérieur est en fait mise en œuvre par Scope. Ce chapitre analyse principalement le mécanisme de surveillance de la portée, l'héritage et la mise en œuvre des événements.
moniteur
1. $ Watch
1.1 Utilisation
// $ watch: function (watchExp, auditeur, objectEquality)
var unwatch = $scope.$watch('aa', function () {}, isEqual);
Ceux qui ont utilisé Angular utiliseront souvent le code ci-dessus, communément appelé "manuellement" en ajoutant l'écoute, et certains autres auditeurs ajoutent automatiquement l'écoute par interpolation ou directive, mais en principe, ils sont les mêmes.
1.2 Analyse du code source
fonction (watchExp, écouteur, objectEquality) {var scope = this, // compiler les chaînes possibles dans fn get = compiletofn (watchExp, 'watch'), array = scope. $$ watchers, watcher = {fn: écouteur, dernier: initWatchVal, // dernière valeur est enregistrée pour faciliter la comparisation suivante: sort, exp: watchex, eq: EQUIVERATEM comparaison ou comparaison de valeur}; LastDirtyWatch = null; if (! isFunction (écouteur)) {var écouterfn = compileTofn (écouteur || noop, 'écouteur'); watcher.fn = function (newVal, oldVal, scope) {écouterfn (scope);}; } if (! array) {array = scope. $$ watchers = []; } // La raison pour laquelle un décalage n'est pas poussé est parce que dans $ digest, la boucle d'observateurs commence à partir de l'arrière / afin de permettre l'exécution de l'observateur nouvellement ajouté dans la boucle actuelle, il est placé à l'avant de la file d'attente.unshift (observateur); // return undatchfn, annuler la fonction de retour de retour deregisterwatch () {arrayRemove (array, watcher); LastDirtyWatch = null; };}À partir du code, $ watch est relativement simple. Il économise principalement l'observateur dans le tableau des observateurs $$
2. $ digest
Lorsque la valeur de l'étendue change, SPOPE n'exécutera pas chaque observateur WatcherFn. Il doit y avoir une notification, et celui qui envoie cette notification est $ digest
2.1 Analyse du code source
Le code source de l'intégralité de $ Digest est d'environ 100 lignes, et la logique principale est concentrée dans la boucle de contrôle sale. Il y a aussi quelques codes mineurs après la boucle, tels que le traitement de PostdigestQueue, nous n'analyserons pas en détail.
La boucle de vérification de la valeur sale signifie que tant qu'il y a une mise à jour de la valeur d'un observateur, une série de chèques doit être exécutée jusqu'à ce qu'aucune mise à jour de valeur ne soit mise à jour. Bien sûr, certaines optimisations ont été faites pour réduire les chèques inutiles.
Code:
// Entrez la boucle $ digest et marquez-le pour empêcher l'entrée répétée dans BeginPhase ('$ digest'); LastDirTyWatch = null; // la boucle de vérification de la valeur sale démarre {sale = false; courant = cible; // La boucle asyncqueue omet TraverseScoPESOOP: do {if ((watchers = current. $$ watchers)) {longueur = watchers.length; while (longueur--) {try {watch = watchers [longueur]; Si (watch) {// faire une mise à jour pour déterminer s'il y a une valeur mise à jour, décomposer comme suit // valeur = watch.get (actuel), dernier = watch.last // valeur! == Enfin si c'est vrai, alors déterminez s'il est nécessaire de faire une valeur pour juger la montre.Eq? watch.get (current))! == (last = watch.last) &&! (watch.eq? equals (value, dernier): (typeof value === 'nombre' && typeof last === 'nombre' && isnan (value) && isnan (dernier)))) {sale = true; // Enregistrez quels changements dans cette boucle LastDirTyWatch = Watch; // cache dernière valeur watch.last = watch.eq? copie (valeur, null): valeur; // EXECUTER LIVERNERFN (NewValue, LastValue, Scope) // Si la première exécution est effectuée, alors LastValue est également défini sur NewValue Watch.fn (valeur, ((dernier === INITWATCHVAL)? Valeur: Last), Current); // ... watchLog omits if (watch.get. $$ undatch) stablewatchescandidates.push ({watch: watch, array: watchers}); } // C'est l'optimisation pour réduire l'observateur // Si la dernière montre mise à jour dans la boucle précédente n'a pas changé, c'est-à-dire qu'il n'y a pas de nouvelle montre mise à jour dans ce tour // Cela signifie que les montres entières ont été stables et ne seront pas mises à jour, et la boucle se termine ici. Les montres restantes n'ont pas besoin d'être vérifiées ailleurs si (watch === LastDirtyWatch) {sale = false; Break Traverscopesloop; }}} catch (e) {clearphase (); $ exceptionhandler (e); }}} // Cette section est un peu emmêlée, ce qui réalise en fait la profondeur-première traction // a -> [b-> d, c-> e] // ordre d'exécution a, b, d, c, e // obtenir le premier enfant à chaque fois. S'il n'y a pas de frère qui ne va pas, s'il n'y a pas de frère, reculez sur la couche précédente et déterminez s'il y a des frères dans la couche. S'il n'y a pas, continuez à se retirer jusqu'à ce qu'il se retire vers la portée de départ. À l'heure actuelle, suivant == null, donc la boucle de lunettes sera quittera si (! (Next = (Current. $$ Childhead || (Current! == Target && Current. $$ NEXTSIBLING))))) {while (Current! == Target &&! (Next = Current. $$ NEXTSIBLING))) {Current = Current. $ Parent; }}} while ((current = suivant)); // Break TRAVERSCOPESSLOOP va directement ici // Déterminez s'il est toujours dans une boucle de valeur sale et a dépassé le nombre maximum de vérifications TTL par défaut 10 if ((sale || asyncqueue.length) &&! (ttl--)) {clearphase (); Throw $ rootscopermingerr ('infdig', '{0} $ digest () itérations atteintes. }} while (sale || asyncqueue.length); // Fin de la boucle // Mark Exit Digest Loop Clearphase ();Il y a des boucles à 3 couches dans le code ci-dessus
La première couche juge sale, s'il y a une valeur sale, continuez à boucler
faire {
// ...
} tandis que (sale)
La deuxième couche détermine si la portée a été traversée. Le code a été traduit. Bien qu'il soit toujours diffusé, il peut être compris.
faire {
// ....
if (actuel. $$ childhead) {
suivant = courant. $$ CHILDEAD;
} else if (actuel! == Target && Current. $$ nextSibling) {
suivant = courant. $$ NEXTSIBLING;
}
while (! Suivant && Current! == Target &&! (Next = Current. $$ NEXTSIBLING)) {
courant = courant. $ parent;
}
} while (current = suivant);
La troisième couche de lans de portée de Loop Watchers
length = watchers.length;
while (longueur--) {
essayer {
watch = watchers [longueur];
// ... omis
} catch (e) {
clearphase ();
$ exceptionhandler (e);
}
}
3. $ EVASYNC
3.1 Analyse du code source
$ evasync est utilisé pour une exécution retardée, le code source est le suivant:
function (expr) {if (! $ rootscope. $$ phase &&! $ rootscope. $$ asyncqueue.length) {$ Browser.defer (function () {if ($ rootscope. $$ asyncqueue.length) {$ rootscope. $ digest ();}}); } ce. $$ asyncqueue.push ({Scope: This, expression: expr});}En jugeant si Dirty Check est déjà en cours d'exécution, ou que quelqu'un a déclenché $ EVASYNC
if (! $ rootscope. $$ phase &&! $ rootscope. $$ asyncqueue.length) $ Browser.defer est de modifier l'ordre d'exécution en appelant setTimeout $ Browser.defer (function () {// ...});Si vous n'utilisez pas de report, alors
function (exp) {queue.push ({scope: this, expression: exp}); this. $ digest ();} scope. $ evasync (fn1); scope. $ evasync (fn2); // le résultat est // $ digest ()> fn1> $ digest ()> fn2 // mais l'effet réel doit être obtenu: $ digest ()>> fn2Le contenu de l'async a été omis dans la section précédente $ digest, qui est situé dans la première couche de boucle
while (asyncqueue.length) {try {asyncTask = asyncqueue.shift (); asynctask.scope. $ eval (asynctask.expression); } catch (e) {clearphase (); $ exceptionhandler (e); } lastDirtyWatch = null;}Simple et facile à comprendre, apparaissez AsyncTask pour l'exécution.
Mais il y a un détail ici, pourquoi est-il mis en place comme ça? La raison est la suivante. Si un nouvel asyncTask est ajouté lorsque le WatchX est exécuté dans une certaine boucle, LastDirtyWatch = WatchX sera défini à ce moment. L'exécution de cette tâche entraînera l'exécution d'une nouvelle valeur dans la montre ultérieure de WatchX. S'il n'y a pas de code suivant, la boucle sautera de la boucle suivante pour LastDirtyWatch (WatchX) et Dirty == FALSE pour le moment.
LastDirtyWatch = null;
Il y a aussi un détail ici, pourquoi boucle au premier niveau? Parce que la portée avec la relation d'héritage a $$ asyncqueue, tout est monté sur la racine, il n'a donc pas besoin d'être exécuté dans la couche de portée de la couche suivante.
2. Héritage
La portée est héritable, telle que $ Parentscope et $ childscope deux lunettes. Lorsque $ childscope.fn est appelé s'il n'y a pas de méthode FN dans $ childscope, alors allez à $ parents pour trouver la méthode.
Recherchez la couche par calque jusqu'à ce que vous trouviez l'attribut requis. Cette fonctionnalité est mise en œuvre en utilisant les caractéristiques de l'héritage prototype de Javascipt.
Code source:
fonction (isolat) {var childScope, enfant; if (isolat) {child = new scope (); enfant. $ root = this. $ root; // L'asyncqueue et le postdigestqueue de l'isolat sont également la racine publique et d'autres enfants indépendants. $$ asyncqueue = this. $$ asyncqueue; Enfant. $$ postdigestqueue = this. $$ postdigestqueue; } else {if (! this. $$ childscopeclass) {this. $$ childscopeclass = function () {// nous pouvons voir quels attributs sont uniques à l'isolement, tels que les observateurs $$, ils seront donc surveillés indépendamment. This. $$$ watchers = this. $$ nextSibling = this. $$ childhead = this. $$ childail = null; this. $$ écouteurs = {}; this. $$ écouteur = {}; this. $ id = nextUid (); this. $$ childScopeClass = null; }; this. $$ childscopeclass.prototype = this; } child = new this. $$ childScopeClass (); } // Définit diverses relations père-fils et frère, ce qui est très désordonné! enfant ['this'] = enfant; enfant. $ parent = this; Enfant. $$ PRIVANT = CECI. $$ Childtail; if (this. $$ childhead) {this. $$ childail. $$ nextsibling = enfant; ce. $$ Childtail = enfant; } else {this. $$ childhead = this. $$ childail = enfant; } return enfant;}Le code est clair, les principaux détails sont les attributs doivent être indépendants et lesquels doivent être basés sur les bases.
Le code le plus important:
this.$$childScopeClass.prototype = this;
L'héritage est réalisé de cette manière.
3. Mécanisme d'événements
3,1 $ sur
function (name, écouteur) {var NamedListeners = this. $$ écouteurs [name]; if (! NamedListeners) {this. $$ auditeurs [name] = nommé Wisteners = []; } namedLisseners.push (auditeur); Var Current = This; do {if (! Current. $$ écouteur [name]) {actuel. $$ écouteur [name] = 0; } Current. $$ écoutère de countour [name] ++; } while ((current = current. $ parent)); var self = this; return function () {NamedLisseners [indexof (NamedLisseners, auditeur)] = null; DecmenmentListenerCount (self, 1, nom); };}Semblable à $ wathc, il est également stocké dans un tableau - nommé Wisteners.
Il y a une autre différence que la portée et tous les parents enregistrent des statistiques d'événements, ce qui est utile lors de la diffusion des événements et une analyse ultérieure.
var current = this; do {if (! current. $$ écouteur [name]) {actuel. $$ écouteur [name] = 0; } Current. $$ écoutère [name] ++;} while ((current = current. $ parent));3,2 $ émettre
$ Emit est un événement de diffusion à la hausse. Code source:
function (name, args) {var vide = [], nommé Wisteners, scope = this, stopPropagation = false, event = {name: name, cibleScope: scope, stopPropagation: function () {stopPropagation = true;}, empêcherDefault: function () {event.defaultPreveted = true; }, defaultPreveted: false}, audinerArgs = concat ([événement], arguments, 1), i, longueur; Do {NamedListeners = Scope. $$ Écouteurs [Name] || vide; event.currentScope = Scope; pour (i = 0, longueur = NamedListeners.length; i <longueur; i ++) {// Après avoir écouté la suppression, il ne sera pas supprimé du tableau, mais est défini sur null, il est donc nécessaire de juger si (! NamedListers [i]) {NamedListeners.splice (i, 1); je--; longueur--; continuer; } essayez {NamedListeners [i] .Apply (null, audinerArgs); } catch (e) {$ exceptionHandler (e); }} // Lorsque la propagation est arrêtée, retournez if (stopPropagation) {event.currentCope = null; événement de retour; } // émettre est le moyen de propager la portée ascendante = Scope. $ parent; } while (scope); event.currentScope = null; événement de retour;}3,3 $ diffuser
$ Broadcast se propage vers l'intérieur, c'est-à-dire se propager à l'enfant, code source:
function (name, args) {var target = this, current = cible, next = cible, event = {name: name, cibleScope: cible, empêtdefault: function () {event.defaultPreveted = true; }, defaultPreveted: false}, audinerArgs = concat ([événement], arguments, 1), auditeurs, i, longueur; while ((current = next)) {event.currentScope = courant; auditeurs = Current. $$ Écouteurs [nom] || []; pour (i = 0, longueur = auditeurs.length; i <longueur; i ++) {// Vérifiez si l'écoute a été annulée if (! écouteurs [i]) {auditeurs.splice (i, 1); je--; longueur--; continuer; } essayez {auditeurs [i] .Apply (null, audinerArgs); } catch (e) {$ exceptionHandler (e); }} // if (next = ((actuel. $$ écouteur de l'écoute [name] && actuel. $$ childhead) || (actuel! == Target && courant. $$ nextSibling)))) {while (current! == Target &&! (Next = Current. $$ NEXTSIBLING)) {Current = Current. $ Parent; }}} event.currentScope = null; événement de retour;}L'autre logique est relativement simple, c'est-à-dire que le code qui est traversé en profondeur est plus déroutant. En fait, c'est la même chose que dans Digest. C'est pour juger s'il y a une écoute sur le chemin. Actuel. $$ écouteur [nom]. À partir du code de $ ci-dessus, nous pouvons voir que tant qu'il y a un enfant sur le chemin et écouter, l'en-tête de chemin a également un numéro. Au contraire, s'il n'est pas indiqué que tous les enfants sur le chemin n'ont pas d'événements d'écoute.
if (! (next = ((actuel. $$ écouteur [name] && courant. $$ childhead) || (current! == Target && Current. $$ nextSibling)))) {while (current! == Target &&! (Next = Current. $$ NEXTSIBLING)) {Current = Current. $ Parent; }}Chemin de propagation:
Root> [a> [a1, a2], b> [b1, b2> [c1, c2], b3]]
Root> a> a1> a2> b> b1> b2> c1> c2> b3
4. $ watchcollection
4.1 Utiliser des exemples
$ scope.Names = ['igor', 'matias', 'mistko', 'James']; $ scope.datacount = 4; $ scope. $ watchcollection ('names', function (newnames, oldnames) {$ scope.datacount = newnames.length;}); attendre ($ scope.datacount) .toequal (4); $ scope. $ digest (); attendre ($ scope.datacount) .toequal (4); $ scope.Names.pop (); $ scope. $ digest ();4.2 Analyse du code source
function (obj, écouteur) {$ watchCollectionInterceptor. $ stateful = true; var self = this; var newValue; var oldvalue; var trèsoldvalue; var trackSeverAldValue = (écouteur.length> 1); var changuetected = 0; var Modigetector = $ parse (obj, $ watchCollectionInterceptor); var internArray = []; var interneObject = {}; var initrun = true; var oldLength = 0; // Déterminez s'il faut changer en fonction de la fonction modifiée modifiée $ watchCollectionInterceptor (_value) {// ... return ModitedEcted; } // Appelez le véritable auditeur via cette méthode en tant que fonction proxy $ watchCollectionAction () {} renvoie ce. $ Watch (Modigetector, $ watchcollectionAction);}La veine principale fait partie du code intercepté ci-dessus. Ce qui suit analyse principalement $ WatchCollectionInterceptor et $ watchCollectionAction
4.3 $ WatchCollectionInterceptor
fonction $ watchCollectionInterceptor (_Value) {newValue = _Value; var NewLength, Key, Bothnan, NewItem, OldItem; if (isUndEfined (newValue)) renvoie; if (! IsObject (newValue)) {if (oldvalue! == newValue) {oldvalue = newValue; Mojettected ++; }} else if (isArrayLILY (newValue)) {if (oldValue! == InternalArray) {oldValue = InternalArray; OldLength = OldValue.Length = 0; Mojettected ++; } newLength = newValue.Length; if (oldLength! == newLength) {ModitedEcted ++; OldValue.Length = OldLength = NewLength; } pour (var i = 0; i <newLength; i ++) {olditem = oldvalue [i]; newItem = newValue [i]; Bothnan = (OldItem! == OldItem) && (NewItem! == NewItem); if (! Bothnan && (oldItem! == NewItem)) {ModitedEcted ++; OldValue [i] = NewItem; }}} else {if (oldvalue! == internalObject) {oldvalue = internalObject = {}; OldLength = 0; Mojettected ++; } newLength = 0; for (key in newValue) {if (hasownproperty.call (newValue, key)) {newLength ++; newItem = newValue [key]; OldItem = OldValue [Key]; if (key in OldValue) {Bothnan = (oldItem! == OldItem) && (NewItem! == NewItem); if (! Bothnan && (oldItem! == NewItem)) {ModitedEcted ++; OldValue [Key] = NewItem; }} else {oldLength ++; OldValue [Key] = NewItem; Mojettected ++; }}} if (oldLength> newLength) {ModitedEcted ++; for (key in OldValue) {if (! HasownProperty.Call (newValue, key)) {oldLength--; Supprimer OldValue [Key]; }}}} Retour modifiée;}1). Retour dire directement lorsque la valeur n'est pas définie.
2). Lorsque la valeur est un type de base ordinaire, déterminez directement s'il est égal.
3). Lorsque la valeur est un tableau de classe (c'est-à-dire que l'attribut de longueur existe, et que la valeur [i] est également appelée un tableau de classe), l'ancienvalue est initialisé d'abord sans initialisation
if (oldValue! == InternalArray) {OldValue = InternalArray; OldLength = OldValue.Length = 0; modifiée ++;}Comparez ensuite la longueur du tableau, si ce n'est pas égal, il est enregistré comme changeant de modification de ++
if (oldLength! == newLength) {ModitedEcted ++; OldValue.Length = OldLength = NewLength;}Comparez un par un
pour (var i = 0; i <newLength; i ++) {olditem = oldvalue [i]; newItem = newValue [i]; Bothnan = (OldItem! == OldItem) && (NewItem! == NewItem); if (! Bothnan && (oldItem! == NewItem)) {ModitedEcted ++; OldValue [i] = NewItem; }}4). Lorsque la valeur est objet, le processus d'initialisation est similaire à ce qui précède
if (oldvalue! == internalObject) {oldValue = internalObject = {}; OldLength = 0; modifiée ++;}Le prochain traitement est plus habile. Si vous constatez que de nouveaux champs avec de nombreux NEWValue sont ajoutés, ajoutez 1 à OldLength, de sorte que OldLength ajoute et ne soustrait pas. Il est facile de découvrir s'il existe de nouveaux champs dans NewValue. Enfin, supprimez les champs supplémentaires dans OldValue, c'est-à-dire les champs supprimés dans NewValue, puis c'est terminé.
newLength = 0; for (key in newValue) {if (hasownproperty.call (newValue, key)) {newLength ++; newItem = newValue [key]; OldItem = OldValue [Key]; if (key in OldValue) {Bothnan = (oldItem! == OldItem) && (NewItem! == NewItem); if (! Bothnan && (oldItem! == NewItem)) {ModitedEcted ++; OldValue [Key] = NewItem; }} else {oldLength ++; OldValue [Key] = NewItem; Mojettected ++; }}} if (oldLength> newLength) {ModitedEcted ++; for (key in OldValue) {if (! HasownProperty.Call (newValue, key)) {oldLength--; Supprimer OldValue [Key]; }}}4.4 $ WatchCollectionAction
function $ watchCollectionAction () {if (initrun) {initrun = false; Écouteur (NewValue, NewValue, Self); } else {écouteur (newValue, trèsoldValue, self); } // trackServeryoldValue = (écouteur.Length> 1) Vérifiez si la méthode de l'écoute nécessite OldValue // Copier si nécessaire if (TrackVervelValue) {if (! IsObject (newValue)) {trèsoldValue = newValue; } else if (isArrayLILY (newValue)) {trèsoldValue = new Array (newValue.Length); for (var i = 0; i <newValue.length; i ++) {trèsoldValue [i] = newValue [i]; }} else {trèsoldValue = {}; for (var key in newValue) {if (HasownProperty.Call (newValue, key)) {trèsoldValue [key] = newValue [key]; }}}}}}Le code est relativement simple, il s'agit d'appeler auditeur. Lorsque le premier appel est OldValue == NewValue. Pour l'efficacité et la mémoire, il est déterminé si l'auditeur a besoin d'un paramètre OldValue.
5. $ eval & $ appliquer
$ EVAL: fonction (expr, locaux) {return $ parse (expr) (this, locaux);}, $ applique: function (expr) {try {beginphase ('$ applique'); Retour. $ EVAL (EXPR); } catch (e) {$ exceptionHandler (e); } enfin {clearphase (); essayez {$ rootscope. $ digest (); } catch (e) {$ exceptionHandler (e); jeter e; }}}$ Apply Appelle $ Rootscope. $ Digest (), de nombreux livres recommandent d'utiliser $ digest () au lieu d'appeler $ applique (), ce qui est plus efficace.
La logique principale est entièrement dans $ Parse, qui appartient à la fonction d'analyse de syntaxe, et sera analysée séparément à l'avenir.