Introduction
Scope lies in a core position in the ng ecosystem. The underlying layer of the two-way binding claimed by ng to the outside world is actually implemented by scope. This chapter mainly analyzes the scope's watch mechanism, inheritance and event implementation.
monitor
1. $watch
1.1 Use
// $watch: function(watchExp, listener, objectEquality)
var unwatch = $scope.$watch('aa', function () {}, isEqual);
Those who have used angular will often use the code above, commonly known as "manually" adding listening, and some other listeners automatically add listening through interpolation or directive, but in principle they are the same.
1.2 Source code 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 is recorded to facilitate the next comparison get: get, exp: watchExp, eq: !!objectEquality // Is the configuration 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 unshift is not pushed is because in $digest the watchers loop starts from the back/ In order to enable the newly added watcher to be executed in the current loop, it is placed at the front of the queue.unshift(watcher); // Return unwatchFn, cancel listening return function deregisterWatch() { arrayRemove(array, watcher); lastDirtyWatch = null; };}From the code, $watch is relatively simple. It mainly saves the watcher into the $$watchers array
2. $digest
When the scope value changes, scope will not execute each watcher listenerFn by itself. There must be a notification, and the one who sends this notification is $digest
2.1 Source code analysis
The source code of the entire $digest is about 100 lines, and the main logic is concentrated in the dirty check loop. There are also some minor codes after the loop, such as the processing of postDigestQueue, so we will not analyze in detail.
Dirty value check loop means that as long as there is an update to the value of a watcher, a round of checks must be run until no value updates are updated. Of course, some optimizations have been made to reduce unnecessary checks.
Code:
// Enter the $digest loop and mark it to prevent repeated entry into beginPhase('$digest');lastDirtyWatch = null;// The dirty value check loop starts do { dirty = false; current = target; // The asyncQueue loop omits traverseScopesLoop: do { if ((watchers = current.$$watchers)) { length = watchers.length; while (length--) { try { watch = watchers[length]; if (watch) { // Make an update to determine whether there is a value updated, decompose as follows // value = watch.get(current), last = watch.last // value !== last If it is true, then determine whether it is necessary to make a value to judge watch.eq?equals(value, last) // If it is not a judgment of equal values, determine the situation of NaN, that is, NaN !== NaN if ((value = watch.get(current)) !== (last = watch.last) && !(watch.eq ? equals(value, last) : (typeof value === 'number' && typeof last === 'number' && isNaN(value) && isNaN(last)))) { dirty = true; // Record which watch changes in this loop lastDirtyWatch = watch; // Cache last value watch.last = watch.eq ? copy(value, null) : value; // Execute listenerFn(newValue, lastValue, scope) // If the first execution is performed, then lastValue is also set to newValue watch.fn(value, ((last === initWatchVal) ? value : last), current); // ... watchLog omits if (watch.get.$$unwatch) stableWatchesCandidates.push({watch: watch, array: watchers}); } // This is the optimization to reduce watcher// If the last updated watch in the previous loop has not changed, that is, there is no new updated watch in this round // Then it means that the entire watches have been stable and will not be updated, and the loop ends here. The remaining watches do not need to be checked else if (watch === lastDirtyWatch) { dirty = false; break traverseScopesLoop; } } } catch (e) { clearPhase(); $exceptionHandler(e); } } } // This section is a bit tangled, which is actually realizing depth-first traversal // A->[B->D,C->E] // Execution order A, B, D, C, E // Get the first child each time. If there is not a nextSibling brother, if there is no brother, then retreat to the previous layer and determine whether there are brothers in the layer. If there is no, continue to retreat until it retreats to the starting scope. At this time, next==null, so the scopes loop will be exited if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { while(current !== target && !(next = current.$$nextSibling))) { current = current.$parent; } } } while ((current = next)); // break traverseScopesLoop goes directly here // Determine whether it is still in a dirty value loop and has exceeded the maximum number of checks ttl default 10 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); // End of loop // Mark exit digest loop clearPhase();There are 3-layer loops in the above code
The first layer judges dirty, if there is a dirty value, then continue to loop
do {
// ...
} while (dirty)
The second layer determines whether the scope has been traversed. The code has been translated. Although it is still circulated, it can be understood.
do {
// ....
if (current.$$childHead) {
next = current.$$childHead;
} else if (current !== target && current.$$nextSibling) {
next = current.$$nextSibling;
}
while (!next && current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
} while (current = next);
The third layer of loop scope watchers
length = watchers.length;
while (length--) {
try {
watch = watchers[length];
// ... Omitted
} catch (e) {
clearPhase();
$exceptionHandler(e);
}
}
3. $evalAsync
3.1 Source code analysis
$evalAsync is used for delayed 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 whether dirty check is already running, or someone has triggered $evalAsync
if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length)$browser.defer is to change the execution order 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 > fn2The content of async was omitted in the previous section $digest, which is located in the first layer of loop
while(asyncQueue.length) { try { asyncTask = asyncQueue.shift(); asyncTask.scope.$eval(asyncTask.expression); } catch (e) { clearPhase(); $exceptionHandler(e); } lastDirtyWatch = null;}Simple and easy to understand, pop up asyncTask for execution.
But there is a detail here, why is it set up like this? The reason is as follows. If a new asyncTask is added when the watchX is executed in a certain loop, lastDirtyWatch=watchX will be set at this time. Execution of this task will cause a new value to be executed in the subsequent watch of watchX. If there is no following code, then the loop will jump out of the next loop to lastDirtyWatch (watchX), and dirty==false at this time.
lastDirtyWatch = null;
There is also a detail here, why loop in the first level? Because the scope with inheritance relationship has $$asyncQueue, it is all mounted on the root, so it does not need to be executed in the scope layer of the next layer.
2. Inheritance
scope is inheritable, such as $parentScope and $childScope two scopes. When $childScope.fn is called if there is no fn method in $childScope, then go to $parentScope to find the method.
Search up layer by layer until you find the required attribute. This feature is implemented using the characteristics of prototype inheritance of javascipt.
Source code:
function(isolate) { var ChildScope, child; if (isolate) { child = new Scope(); child.$root = this.$root; // The asyncQueue and postDigestQueue of isolate are also public root, and other independent children.$$asyncQueue = this.$$asyncQueue; child.$$postDigestQueue = this.$$postDigestQueue; } else { if (!this.$$childScopeClass) { this.$$childScopeClass = function() { // Here we can see which attributes are unique to isolating, such as $$watchers, so they will be monitored independently. 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 various father-son and brother relationships, which is 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 child;}The code is clear, the main details are which attributes need to be independent and which ones need to be based on the basics.
The most important code:
this.$$childScopeClass.prototype = this;
Inheritance is realized in this way.
3. Event mechanism
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 also stored in an array -- namedListeners.
There is another difference that the scope and all parents save an event statistics, 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 an upward broadcast event. Source code:
function(name, args) { var empty = [], namedListeners, scope = this, stopPropagation = false, event = { name: name, targetScope: scope, stopPropagation: function() {stopPropagation = true;}, preventDefault: function() { 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++) { // After listening to remove, it will not be deleted from the array, but is set to null, so it is necessary to judge if (!namedListeners[i]) { namedListeners.splice(i, 1); i--; length--; continue; } try { namedListeners[i].apply(null, listenerArgs); } catch (e) { $exceptionHandler(e); } } // When the propagation is stopped, return if (stopPropagation) { event.currentScope = null; return event; } // emit is the way to propagate upward scope = scope.$parent; } while (scope); event.currentScope = null; return event;}3.3 $broadcast
$broadcast is propagating inward, that is, propagating to child, source code:
function(name, args) { var target = this, current = target, next = target, event = { name: name, targetScope: 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 the listening has been cancelled if (!listeners[i]) { listeners.splice(i, 1); i--; length--; continue; } try { listeners[i].apply(null, listenerArgs); } catch(e) { $exceptionHandler(e); } } // If (next = ((current.$$listenerCount[name] && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while(current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } } event.currentScope = null; return event;}The other logic is relatively simple, that is, the code that is traversed in depth is more confusing. In fact, it is the same as in digest. It is to judge whether there is a listening on the path. Current.$$listenerCount[name]. From the above $on code, we can see that as long as there is a child on the path and listen, the path header also has a number. On the contrary, if it is not stated that all children on the path have no listening events.
if (!(next = ((current.$$listenerCount[name] && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while(current !== target && !(next = current.$$nextSibling)) { current = current.$parent; }}Propagation 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 Use examples
$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 code 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; // Determine whether to change based on the returned changeDetected function $watchCollectionInterceptor(_value) { // ... return changeDetected; } // Call the real listener through this method as a proxy function $watchCollectionAction() { } return this.$watch(changeDetector, $watchCollectionAction);}The main vein is part of the code intercepted above. The following mainly analyzes $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 !== 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 !== 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). Return directly when the value is undefined.
2). When the value is an ordinary basic type, directly determine whether it is equal.
3). When the value is a class array (that is, the length attribute exists, and value[i] is also called a class array), the oldValue is initialized first without initialization
if (oldValue !== internalArray) { oldValue = internalArray; oldLength = oldValue.length = 0; changeDetected++;}Then compare the array length, if not equal, it is recorded as changing changedDetected++
if (oldLength !== newLength) { changeDetected++; oldValue.length = oldLength = newLength;}Compare one by one
for (var i = 0; i < newLength; i++) { oldItem = oldValue[i]; newItem = newValue[i]; bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; oldValue[i] = newItem; }}4). When the value is object, the initialization process is similar to the above
if (oldValue !== internalObject) { oldValue = internalObject = {}; oldLength = 0; changeDetected++;}The next processing is more skillful. If you find that new fields with many newValue are added, add 1 to oldLength, so that oldLength only adds and does not subtract. It is easy to find whether there are new fields in newValue. Finally, remove the extra fields in oldValue, that is, the deleted fields in newValue, and then it is over.
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)) { 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) Check whether the listener method requires oldValue // Copy if necessary 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, it is to call listenerFn. When the first call is oldValue == newValue. For efficiency and memory, it is determined whether listener needs oldValue parameter.
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 finally calls $rootScope.$digest(), so many books recommend using $digest() instead of calling $apply(), which is more efficient.
The main logic is all in $parse, which belongs to the syntax parsing function, and will be analyzed separately in the future.