Brief introduction
In Ng's ecology, scope is at a core position, the bottom of the two-way binding of NG is actually the scope of implementation, this chapter mainly on the scope of the watch mechanism, inheritance and the implementation of the event for analysis.
Listening
1. $watch
1.1 Use
$watch: Function (watchexp, listener, objectequality)
var unwatch = $scope.$watch('aa', function () {}, isEqual);
Use of angular will often the above code, commonly known as "manual" to add listening, others are interpolation or directive automatically add listening, but the principle is the same.
1.2 Source Analysis
function (watchexp, Listener, objectequality) {
var scope = This,
//compile possible strings into fn get
= Compiletofn (watchexp , ' Watch '),
array = scope.$ $watchers,
watcher = {
Fn:listener,
last:initwatchval, //Last value record, Convenient next time compare
Get:get,
exp:watchexp,
eq:!! Objectequality//configuration is reference comparison or value comparison
};
Lastdirtywatch = null;
if (!isfunction (listener)) {
var listenfn = compiletofn (Listener | | noop, ' listener ');
Watcher.fn = function (newval, oldval, scope) {LISTENFN (scope);}
if (!array) {
array = scope.$ $watchers = [];
}
The reason why using unshift is not push is because in $digest the Watchers loop is started from the back
/in order to make the newly added watcher can also be executed in the secondary loop so that it is placed at the front of the queue
Array.unshift ( Watcher);
Returns the UNWATCHFN, suppresses the listener return
function Deregisterwatch () {
arrayremove (array, watcher);
Lastdirtywatch = null;}
;
}
From the code $watch is still relatively simple, mainly to save the watcher to the $ $watchers array
2. $digest
When the value of scope changes, scope does not perform each watcher LISTENERFN itself, and must have a notification, and the notification is sent $digest
2.1 Source Analysis
The entire $digest source code is almost 100 lines, the main logic is concentrated in the "Dirty Check Loop" (Dirty check loop), after the loop also some minor code, such as postdigestqueue processing and so do not make detailed analysis.
The dirty value checks the loop, meaning that as long as there is an update on a watcher value, a round of checks is run until no value is updated, and of course some optimizations are made to reduce unnecessary checks.
Code:
Enter the $digest cycle marking to prevent repeated entry into the Beginphase (' $digest ');
Lastdirtywatch = null;
Dirty value Check loop start do {dirty = false;
current = target;
Asyncqueue Loops Omit Traversescopesloop:do {if (watchers = current.$ $watchers)) {length = Watchers.length;
while (length--) {try {watch = Watchers[length]; if (watch) {//update to determine if there is a value update, the breakdown is as follows//value = Watch.get (current), last = watch.last//value!== , determine whether a value should be judged watch.eq?equals (value, last)//if it is not a value equality judgment, the case of Nan!== nan if (value = Watch.get (curren T)!== (last = watch.last) &&!
(Watch.eq equals (value, last): (typeof value = = ' Number ' && typeof last = = ' number '
&& isNaN (value) && isNaN (last))) {dirty = true;
Record which watch in this cycle change lastdirtywatch = watch; Cache last Value Watch.last = Watch.eq?
Copy (value, NULL): value;
Executive LISTENERFN (NewValue, Lastvalue, scope) If it is executed for the first time, then Lastvalue is also set to NewValue Watch.fn ((last = = = Initwatchval), current); ... watchlog omit if (watch.get.$ $unwatch) Stablewatchescandidates.push ({watch:watch, Array:wat
Chers}); //This is the reduction of watcher optimization//If the last update of the previous cycle watch has not changed, that is, there is no new updated watch//Then the whole watches has been stable will not be updated, this cycle is over, leaving
Watch, you don't have to check for else if (watch = = Lastdirtywatch) {dirty = false;
Break Traversescopesloop;
catch (E) {clearphase ();
$exceptionHandler (e); }}//This is a bit of a detour, in fact, to achieve depth first traversal//a->[b->d,c->e]///execution order A,b,d,c,e//each priority to get first child, if not then get Nextsibli Ng Brothers, if even the brothers are gone, then back to the upper level and determine whether the layer has brothers, no words continue to retreat until the beginning of the scope, then Next==null, so will exit the scopes cycle if (!
Next = (current.$ $childHead | | (Current!== target && current.$ $nextSibling))) {while, current!== target &&! (
Next = current.$ $nextSibling)) {current = current. $parent; }}} WHile ((current = next)); Break Traversescopesloop directly to this side//judge is not still in the dirty value loop, and has exceeded the maximum number of checks TTL default ten if (Dirty | | asyncqueue.length) &&! (
ttl--)) {clearphase (); Throw $rootScopeMinErr (' infdig ', ' {0} ' $digest () iterations reached.
aborting!\n ' + ' watchers fired in the last 5 iterations: {1} ', TTL, Tojson (Watchlog)); } while (Dirty | | asyncqueue.length);
Loop End/Mark Exit Digest Cycle Clearphase ();
There are 3 layers of loops in the above code
The first level of judgment dirty, if there is dirty value then continue to cycle
do {
// ...
} while (dirty)
The second level determines whether scope has been traversed, the code translated, although still around but can understand
do {
// ....
if (current.$ $childHead) {
Next = current.$ $childHead;
else if (current!== target && current.$ $nextSibling) {
Next = current.$ $nextSibling;
}
while (!next &&!== target &&! ( Next = current.$ $nextSibling)) {
Current = current. $parent;
}
while (current = next);
Watchers of the third-tier cycle scope
length = Watchers.length;
while (length--) {
try {
Watch = Watchers[length];
// ... Omitted
catch (e) {
Clearphase ();
$exceptionHandler (e);
}
}
3. $evalAsync
3.1 Source Analysis
$evalAsync used for deferred execution, the source code is as follows:
function (expr) {
if (! $rootScope. $ $phase &&! $rootScope. $ $asyncQueue. Length) {
$browser. Defer ( function () {
if ($rootScope. $ $asyncQueue. Length) {
$rootScope. $digest ();}}
);
this.$ $asyncQueue. Push ({scope:this, expression:expr});
By judging if there is already a dirty check running, or someone has already triggered the $evalasync
if (! $rootScope. $ $phase &&! $rootScope. $ $asyncQueue. Length)
$browser. Defer is to change the order of execution by calling SetTimeout
$browser. Defer (function () {
//...
});
If you are not using defer, then
Function (exp) {
Queue.push ({scope:this, expression:exp});
this. $digest ();
}
Scope. $evalAsync (FN1);
Scope. $evalAsync (FN2);
The result is
//$digest () > Fn1 > $digest () > Fn2
//But the actual effect needs to be achieved: $digest () > Fn1 > Fn2
The contents of the async are omitted from the $digest in the previous section and are located in the first layer loop
while (asyncqueue.length) {
try {
asynctask = Asyncqueue.shift ();
Asynctask.scope. $eval (asynctask.expression);
catch (e) {
clearphase ();
$exceptionHandler (e);
}
Lastdirtywatch = null;
}
Easy to understand, pop-up asynctask for execution.
But there is a detail here, why is it so set up? The reason for this is that if you add 1 asynctask to Watchx in a loop, then Lastdirtywatch=watchx is set up, and that task execution causes a WATCHX subsequent watch to execute the new value, if there is no code below, Then the next loop to Lastdirtywatch (WATCHX) jumps out of the loop and dirty==false at this point.
Lastdirtywatch = null;
There is also a detail here, why in the first layer of circulation? Because the scope of the inheritance relationship is $ $asyncQueue common, it is mounted on root and does not need to be executed at the next level of the scope layer.
2. The nature of inheritance
Scope has inheritance, such as $parentScope, $childScope two scope, and when $childScope. Fn is called, if the $childScope does not have the FN method, then the method is found on the $parentScope.
This layer looks up until the desired attribute is found. This feature is implemented using the characteristics of Javascirpt's prototype inheritance.
Source:
function (isolate) {var childscope, child;
if (isolate) {child = new Scope ();
Child, $root = this. $root;
The Asyncqueue and postdigestqueue of isolate are also common root, and other independent child.$ $asyncQueue = this.$ $asyncQueue;
child.$ $postDigestQueue = this.$ $postDigestQueue; } else {if (!this.$ $childScopeClass) {this.$ $childScopeClass = function () {//) Here you can see which properties are isolated, such as $ $watchers, so
On the independent monitoring, this.$ $watchers = this.$ $nextSibling = this.$ $childHead = this.$ $childTail = null;
this.$ $listeners = {};
this.$ $listenerCount = {};
this. $id = Nextuid ();
this.$ $childScopeClass = null;
};
this.$ $childScopeClass. prototype = this;
Child = new this.$ $childScopeClass ();
//Set up a variety of father and son, brother relationship, very messy!
Child[' this '] = child;
Child, $parent = this;
child.$ $prevSibling = this.$ $childTail;
if (this.$ $childHead) {this.$ $childTail. $ $nextSibling = child;
this.$ $childTail = child;
else {this.$ $childHead = this.$ $childTail = child;
return to child;
}
The code is also clear that the main details are which attributes need to be independent and which need to be based.
The most important code:
this.$$childScopeClass.prototype = this;
That's how inheritance is done.
3. Event mechanisms
3.1 $on
function (name, listener) {
var namedlisteners = this.$ $listeners [name];
if (!namedlisteners) {
this.$ $listeners [name] = Namedlisteners = [];
}
Namedlisteners.push (listener);
var current = this;
Do {
if (!current.$ $listenerCount [name]) {
current.$ $listenerCount [name] = 0;
}
current.$ $listenerCount [name]++;
} while (current = current. $parent));
var self = this;
return function () {
namedlisteners[indexof (namedlisteners, listener)] = null;
Decrementlistenercount (self, 1, name);}
;
}
Similar to $WATHC, it is stored in an array--namedlisteners.
There's a different place. This scope and all of the parent have saved the statistics of an event, which is useful when broadcasting events, and subsequent analysis.
var current = this;
Do {
if (!current.$ $listenerCount [name]) {
current.$ $listenerCount [name] = 0;
}
current.$ $listenerCount [name]++;
} while (current = current. $parent));
3.2 $emit
$emit is to broadcast events up. Source:
function (name, args) {var empty = [], namedlisteners, scope = this, Stoppropagation = False, event = { Name:name, Targetscope:scope, Stoppropagation:function () {stoppropagation = true;}, Preventdefault:functio
N () {event.defaultprevented = true;
}, Defaultprevented:false}, Listenerargs = Concat ([Event], arguments, 1), I, length;
do {namedlisteners = scope.$ $listeners [name] | | empty;
Event.currentscope = scope; For (i=0, Length=namedlisteners.length, i<length; i++) {//When the monitor remove is not removed from the array, it is set to NULL, so you need to judge if (!namedliste
Ners[i]) {Namedlisteners.splice (I, 1);
i--;
length--;
Continue
try {namedlisteners[i].apply (null, Listenerargs);
catch (E) {$exceptionHandler (e);
The return if (stoppropagation) {event.currentscope = null when stopping propagation;
return event;
//emit is the upward propagation mode scope = scope. $parent;
while (scope);
Event.currentscope = null;
return event;}
3.3 $broadcast
$broadcast is to spread inward, that is, to child transmission, source code:
function (name, args) {var target = this, current = target, next = target, event = {Name:name, target
Scope:target, Preventdefault:function () {event.defaultprevented = true;
}, Defaultprevented:false}, Listenerargs = Concat ([Event], arguments, 1), listeners, I, length;
while (current = next) {Event.currentscope = current; listeners = current.$ $listeners [name] | |
[]; For (i=0, length = listeners.length; i<length; i++) {//Check if (!listeners[i]) {Listeners.splic has been canceled
E (I, 1);
i--;
length--;
Continue
try {listeners[i].apply (null, Listenerargs);
catch (E) {$exceptionHandler (e); The IF (!) is already in the digest.
Next = ((current.$ $listenerCount [name] && current.$ $childHead) | | (Current!== target && current.$ $nextSibling))) {while, current!== target &&! (
Next = current.$ $nextSibling)) {current = current. $parent; }} Event. CurrentScope = null;
return event;
}
Other logic is relatively simple, that is, in depth traversal of the code comparison around, in fact, as in Digest, is more in the path to determine whether there is listening, current.$ $listenerCount [name], from the above $on code, As long as there is a child on the path listening, the path header is also numeric, and conversely, if there is no indication that all the child on the path does not have a listener event.
if (!) ( Next = ((current.$ $listenerCount [name] && current.$ $childHead) | |
(Current!== target && current.$ $nextSibling))) {While
, current!== target &&! ( Next = current.$ $nextSibling)) {Current
= current. $parent;
}
}}
Propagate path:
ROOT>[A>[A1,A2], B>[B1,B2>[C1,C2],B3]]
Root > A > a1 > a2 > B > B1 > B2 > C1 > C2 > B3
4. $watchCollection
4.1 Using the example
$scope. Names = [' Igor ', ' Matias ', ' Misko ', ' James '];
$scope. datacount = 4;
$scope. $watchCollection (' names ', function (Newnames, oldnames) {
$scope. datacount = newnames.length;
});
Expect ($scope. Datacount). toequal (4);
$scope. $digest ();
Expect ($scope. Datacount). toequal (4);
$scope. Names.pop ();
$scope. $digest ();
Expect ($scope. Datacount). toequal (3);
4.2 Source Analysis
function (obj, listener) {
$watchCollectionInterceptor. $stateful = true;
var self = this;
var newvalue;
var OldValue;
var Veryoldvalue;
var trackveryoldvalue = (Listener.length > 1);
var changedetected = 0;
var changedetector = $parse (obj, $watchCollectionInterceptor);
var internalarray = [];
var internalobject = {};
var initrun = true;
var oldlength = 0;
Determines whether the
function $watchCollectionInterceptor (_value) {
//...
) is changed according to the returned changedetected. return changedetected;
}
Call real listener through this method, as Agent
function $watchCollectionAction () {} return this
. $watch (Changedetector, $watchCollectionAction);
}
The main thread is the part of the code intercepted above, the following main analysis $watchCollectionInterceptor and $watchCollectionAction
4.3 $watchCollectionInterceptor
function $watchCollectionInterceptor (_value) {newvalue = _value;
var newlength, Key, Bothnan, NewItem, Olditem;
if (isundefined (NewValue)) return;
if (!isobject (NewValue)) {if (OldValue!== newvalue) {oldValue = newvalue;
changedetected++;
} else if (Isarraylike (NewValue)) {if (OldValue!== internalarray) {oldValue = Internalarray;
oldlength = oldvalue.length = 0;
changedetected++;
} newlength = Newvalue.length;
if (oldlength!== newlength) {changedetected++;
Oldvalue.length = Oldlength = Newlength;
for (var i = 0; i < newlength i++) {olditem = Oldvalue[i];
NewItem = Newvalue[i];
Bothnan = (Olditem!== olditem) && (NewItem!==);
if (!bothnan && (Olditem!== newitem)) {changedetected++;
Oldvalue[i] = NewItem;
}} else {if (OldValue!== internalobject) {OldValue = Internalobject = {};
oldlength = 0;
changedetected++;
} 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!==);
if (!bothnan && (Olditem!== newitem)) {changedetected++;
Oldvalue[key] = NewItem;
} else {oldlength++;
Oldvalue[key] = NewItem;
changedetected++;
}} if (Oldlength > Newlength) {changedetected++;
For (key in OldValue) {if (!hasownproperty.call (NewValue, key)) {oldlength--;
Delete Oldvalue[key];
}}} return changedetected;
}
1). Returns directly when the value is undefined.
2. The direct judgment is equal when the normal basic type is on duty.
3. The value is an array of classes (that is, there is a length property, and Value[i] is also called a class array), first initialization is not initialized oldvalue
if (OldValue!== internalarray) {
oldValue = Internalarray;
oldlength = oldvalue.length = 0;
changedetected++;
}
The length of the array is then compared to the range of words that have changed changedetected++
if (oldlength!== newlength) {
changedetected++;
Oldvalue.length = Oldlength = Newlength;
}
And then compare it one by one
for (var i = 0; i < newlength i++) {
olditem = oldvalue[i];
NewItem = Newvalue[i];
Bothnan = (Olditem!== olditem) && (NewItem!==);
if (!bothnan && (Olditem!== newitem)) {
changedetected++;
Oldvalue[i] = NewItem
}
}
4. When the value is object, similar to the above initialization processing
if (OldValue!== internalobject) {
OldValue = Internalobject = {};
oldlength = 0;
changedetected++;
}
The next processing is more skillful, whenever found newvalue many new fields, in Oldlength plus 1, so that oldlength only add not minus, it is easy to find out whether there is a new newvalue field, and finally put out the oldvalue of the field is The deleted field in the NewValue is removed and ends.
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!==);
if (!bothnan && (Olditem!== newitem)) {
changedetected++;
Oldvalue[key] = NewItem
}
} else {
oldlength++;
Oldvalue[key] = NewItem;
changedetected++
}
}} if (Oldlength > Newlength) {
changedetected++;
For (key in OldValue) {
if (!hasownproperty.call (NewValue, key)) {
oldlength--;
Delete Oldvalue[key];}}
4.4 $watchCollectionAction
function $watchCollectionAction () {
if (initrun) {
initrun = false;
Listener (NewValue, newvalue, self);
} else {
Listener (newvalue, veryoldvalue, self);
}
Trackveryoldvalue = (Listener.length > 1) See if the listener method requires oldvalue
//If necessary to replicate if
(Trackveryoldvalue) {
if (!isobject (NewValue)) {
veryoldvalue = newvalue;
} else if (Isarraylike (NewValue)) {
Veryoldvalue = new Array (newvalue.length);
for (var i = 0; i < newvalue.length i++) {
veryoldvalue[i] = Newvalue[i];
}
} else {
veryoldvalue = {};
for (var key in NewValue) {
if (Hasownproperty.call (NewValue, key)) {
Veryoldvalue[key] = Newvalue[key];
}
}
}
}
}
The code is relatively simple, that is, call LISTENERFN, the first call OldValue = = newvalue, for efficiency and memory to determine whether the next listener need oldValue parameters
5. $eval & $apply
$eval: function (expr, locals) {return
$parse (expr) (this, locals);
},
$apply: function (expr) {
try {
beginphase (' $apply ');
return this. $eval (expr);
catch (e) {
$exceptionHandler (e);
} finally {
clearphase ();
try {
$rootScope. $digest ();
} catch (e) {
$exceptionHandler (e);
throw e;
}}}
$apply the last call to $rootScope. $digest (), so many books recommend using $digest () instead of calling $apply (), which is more efficient.
The main logic is that the $parse belongs to the parsing function, followed by individual analysis.