'use strict'; /** * DESIGN NOTES * * The design decisions behind the scope ware heavily favored for speed and memory consumption. * * The typical use of scope is to watch the expressions, which most of the time return the same * value as last time so we optimize the operation. * * Closures construction is expensive from speed as well as memory: * - no closures, instead ups prototypical inheritance for API * - Internal state needs to be stored on scope directly, which means that private state is * exposed as $$____ properties * * Loop operations are optimized by using while(count--) { ... } * - this means that in order to keep the same order of execution as addition we have to add * items to the array at the begging (shift) instead of at the end (push) * * Child scopes are created and removed often * - Using array would be slow since inserts in meddle are expensive so we use linked list * * There are few watches then a lot of observers. This is why you don't want the observer to be * implemented in the same way as watch. Watch requires return of initialization function which * are expensive to construct. */ /** * @ngdoc object * @name ng.$rootScopeProvider * @description * * Provider for the $rootScope service. */ /** * @ngdoc function * @name ng.$rootScopeProvider#digestTtl * @methodOf ng.$rootScopeProvider * @description * * Sets the number of digest iteration the scope should attempt to execute before giving up and * assuming that the model is unstable. * * The current default is 10 iterations. * * @param {number} limit The number of digest iterations. */ /** * @ngdoc object * @name ng.$rootScope * @description * * Every application has a single root {@link ng.$rootScope.Scope scope}. * All other scopes are child scopes of the root scope. Scopes provide mechanism for watching the model and provide * event processing life-cycle. See {@link guide/scope developer guide on scopes}. */ function $RootScopeProvider(){ var TTL = 10; this.digestTtl = function(value) { if (arguments.length) { TTL = value; } return TTL; }; this.$get = ['$injector', '$exceptionHandler', '$parse', function( $injector, $exceptionHandler, $parse) { /** * @ngdoc function * @name ng.$rootScope.Scope * * @description * A root scope can be retrieved using the {@link ng.$rootScope $rootScope} key from the * {@link AUTO.$injector $injector}. Child scopes are created using the * {@link ng.$rootScope.Scope#$new $new()} method. (Most scopes are created automatically when * compiled HTML template is executed.) * * Here is a simple scope snippet to show how you can interact with the scope. *
angular.injector(['ng']).invoke(function($rootScope) {
var scope = $rootScope.$new();
scope.salutation = 'Hello';
scope.name = 'World';
expect(scope.greeting).toEqual(undefined);
scope.$watch('name', function() {
scope.greeting = scope.salutation + ' ' + scope.name + '!';
}); // initialize the watch
expect(scope.greeting).toEqual(undefined);
scope.name = 'Misko';
// still old value, since watches have not been called yet
expect(scope.greeting).toEqual(undefined);
scope.$digest(); // fire all the watches
expect(scope.greeting).toEqual('Hello Misko!');
});
*
*
* # Inheritance
* A scope can inherit from a parent scope, as in this example:
*
var parent = $rootScope;
var child = parent.$new();
parent.salutation = "Hello";
child.name = "World";
expect(child.salutation).toEqual('Hello');
child.salutation = "Welcome";
expect(child.salutation).toEqual('Welcome');
expect(parent.salutation).toEqual('Hello');
*
*
*
* @param {Object.
// let's assume that scope was dependency injected as the $rootScope
var scope = $rootScope;
scope.name = 'misko';
scope.counter = 0;
expect(scope.counter).toEqual(0);
scope.$watch('name', function(newValue, oldValue) { scope.counter = scope.counter + 1; });
expect(scope.counter).toEqual(0);
scope.$digest();
// no variable change
expect(scope.counter).toEqual(0);
scope.name = 'adam';
scope.$digest();
expect(scope.counter).toEqual(1);
*
*
*
*
* @param {(function()|string)} watchExpression Expression that is evaluated on each
* {@link ng.$rootScope.Scope#$digest $digest} cycle. A change in the return value triggers a
* call to the `listener`.
*
* - `string`: Evaluated as {@link guide/expression expression}
* - `function(scope)`: called with current `scope` as a parameter.
* @param {(function()|string)=} listener Callback called whenever the return value of
* the `watchExpression` changes.
*
* - `string`: Evaluated as {@link guide/expression expression}
* - `function(newValue, oldValue, scope)`: called with current and previous values as parameters.
*
* @param {boolean=} objectEquality Compare object for equality rather than for reference.
* @returns {function()} Returns a deregistration function for this listener.
*/
$watch: function(watchExp, listener, objectEquality) {
var scope = this,
get = compileToFn(watchExp, 'watch'),
array = scope.$$watchers,
watcher = {
fn: listener,
last: initWatchVal,
get: get,
exp: watchExp,
eq: !!objectEquality
};
// in the case user pass string, we need to compile it, do we really need this ?
if (!isFunction(listener)) {
var listenFn = compileToFn(listener || noop, 'listener');
watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);};
}
if (typeof watchExp == 'string' && get.constant) {
var originalFn = watcher.fn;
watcher.fn = function(newVal, oldVal, scope) {
originalFn.call(this, newVal, oldVal, scope);
arrayRemove(array, watcher);
};
}
if (!array) {
array = scope.$$watchers = [];
}
// we use unshift since we use a while loop in $digest for speed.
// the while loop reads in reverse order.
array.unshift(watcher);
return function() {
arrayRemove(array, watcher);
};
},
/**
* @ngdoc function
* @name ng.$rootScope.Scope#$watchCollection
* @methodOf ng.$rootScope.Scope
* @function
*
* @description
* Shallow watches the properties of an object and fires whenever any of the properties change
* (for arrays this implies watching the array items, for object maps this implies watching the properties).
* If a change is detected the `listener` callback is fired.
*
* - The `obj` collection is observed via standard $watch operation and is examined on every call to $digest() to
* see if any items have been added, removed, or moved.
* - The `listener` is called whenever anything within the `obj` has changed. Examples include adding new items
* into the object or array, removing and moving items around.
*
*
* # 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();
//still at 4 ... no changes
expect($scope.dataCount).toEqual(4);
$scope.names.pop();
$scope.$digest();
//now there's been a change
expect($scope.dataCount).toEqual(3);
*
*
*
* @param {string|Function(scope)} obj Evaluated as {@link guide/expression expression}. The expression value
* should evaluate to an object or an array which is observed on each
* {@link ng.$rootScope.Scope#$digest $digest} cycle. Any shallow change within the collection will trigger
* a call to the `listener`.
*
* @param {function(newCollection, oldCollection, scope)} listener a callback function that is fired with both
* the `newCollection` and `oldCollection` as parameters.
* The `newCollection` object is the newly modified data obtained from the `obj` expression and the
* `oldCollection` object is a copy of the former collection data.
* The `scope` refers to the current scope.
*
* @returns {function()} Returns a de-registration function for this listener. When the de-registration function is executed
* then the internal watch operation is terminated.
*/
$watchCollection: function(obj, listener) {
var self = this;
var oldValue;
var newValue;
var changeDetected = 0;
var objGetter = $parse(obj);
var internalArray = [];
var internalObject = {};
var oldLength = 0;
function $watchCollectionWatch() {
newValue = objGetter(self);
var newLength, key;
if (!isObject(newValue)) {
if (oldValue !== newValue) {
oldValue = newValue;
changeDetected++;
}
} else if (isArray(newValue)) {
if (oldValue !== internalArray) {
// we are transitioning from something which was not an array into array.
oldValue = internalArray;
oldLength = oldValue.length = 0;
changeDetected++;
}
newLength = newValue.length;
if (oldLength !== newLength) {
// if lengths do not match we need to trigger change notification
changeDetected++;
oldValue.length = oldLength = newLength;
}
// copy the items to oldValue and look for changes.
for (var i = 0; i < newLength; i++) {
if (oldValue[i] !== newValue[i]) {
changeDetected++;
oldValue[i] = newValue[i];
}
}
} else {
if (oldValue !== internalObject) {
// we are transitioning from something which was not an object into object.
oldValue = internalObject = {};
oldLength = 0;
changeDetected++;
}
// copy the items to oldValue and look for changes.
newLength = 0;
for (key in newValue) {
if (newValue.hasOwnProperty(key)) {
newLength++;
if (oldValue.hasOwnProperty(key)) {
if (oldValue[key] !== newValue[key]) {
changeDetected++;
oldValue[key] = newValue[key];
}
} else {
oldLength++;
oldValue[key] = newValue[key];
changeDetected++;
}
}
}
if (oldLength > newLength) {
// we used to have more keys, need to find them and destroy them.
changeDetected++;
for(key in oldValue) {
if (oldValue.hasOwnProperty(key) && !newValue.hasOwnProperty(key)) {
oldLength--;
delete oldValue[key];
}
}
}
}
return changeDetected;
}
function $watchCollectionAction() {
listener(newValue, oldValue, self);
}
return this.$watch($watchCollectionWatch, $watchCollectionAction);
},
/**
* @ngdoc function
* @name ng.$rootScope.Scope#$digest
* @methodOf ng.$rootScope.Scope
* @function
*
* @description
* Process all of the {@link ng.$rootScope.Scope#$watch watchers} of the current scope and its children.
* Because a {@link ng.$rootScope.Scope#$watch watcher}'s listener can change the model, the
* `$digest()` keeps calling the {@link ng.$rootScope.Scope#$watch watchers} until no more listeners are
* firing. This means that it is possible to get into an infinite loop. This function will throw
* `'Maximum iteration limit exceeded.'` if the number of iterations exceeds 10.
*
* Usually you don't call `$digest()` directly in
* {@link ng.directive:ngController controllers} or in
* {@link ng.$compileProvider#directive directives}.
* Instead a call to {@link ng.$rootScope.Scope#$apply $apply()} (typically from within a
* {@link ng.$compileProvider#directive directives}) will force a `$digest()`.
*
* If you want to be notified whenever `$digest()` is called,
* you can register a `watchExpression` function with {@link ng.$rootScope.Scope#$watch $watch()}
* with no `listener`.
*
* You may have a need to call `$digest()` from within unit-tests, to simulate the scope
* life-cycle.
*
* # Example
*
var scope = ...;
scope.name = 'misko';
scope.counter = 0;
expect(scope.counter).toEqual(0);
scope.$watch('name', function(newValue, oldValue) {
scope.counter = scope.counter + 1;
});
expect(scope.counter).toEqual(0);
scope.$digest();
// no variable change
expect(scope.counter).toEqual(0);
scope.name = 'adam';
scope.$digest();
expect(scope.counter).toEqual(1);
*
*
*/
$digest: function() {
var watch, value, last,
watchers,
asyncQueue = this.$$asyncQueue,
length,
dirty, ttl = TTL,
next, current, target = this,
watchLog = [],
logIdx, logMsg;
beginPhase('$digest');
do { // "while dirty" loop
dirty = false;
current = target;
while(asyncQueue.length) {
try {
current.$eval(asyncQueue.shift());
} catch (e) {
$exceptionHandler(e);
}
}
do { // "traverse the scopes" loop
if ((watchers = current.$$watchers)) {
// process our watches
length = watchers.length;
while (length--) {
try {
watch = watchers[length];
// Most common watches are on primitives, in which case we can short
// circuit it with === operator, only when === fails do we use .equals
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;
watch.last = watch.eq ? copy(value) : value;
watch.fn(value, ((last === initWatchVal) ? value : last), current);
if (ttl < 5) {
logIdx = 4 - ttl;
if (!watchLog[logIdx]) watchLog[logIdx] = [];
logMsg = (isFunction(watch.exp))
? 'fn: ' + (watch.exp.name || watch.exp.toString())
: watch.exp;
logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last);
watchLog[logIdx].push(logMsg);
}
}
} catch (e) {
$exceptionHandler(e);
}
}
}
// Insanity Warning: scope depth-first traversal
// yes, this code is a bit crazy, but it works and we have tests to prove it!
// this piece should be kept in sync with the traversal in $broadcast
if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) {
while(current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
} while ((current = next));
if(dirty && !(ttl--)) {
clearPhase();
throw Error(TTL + ' $digest() iterations reached. Aborting!\n' +
'Watchers fired in the last 5 iterations: ' + toJson(watchLog));
}
} while (dirty || asyncQueue.length);
clearPhase();
},
/**
* @ngdoc event
* @name ng.$rootScope.Scope#$destroy
* @eventOf ng.$rootScope.Scope
* @eventType broadcast on scope being destroyed
*
* @description
* Broadcasted when a scope and its children are being destroyed.
*/
/**
* @ngdoc function
* @name ng.$rootScope.Scope#$destroy
* @methodOf ng.$rootScope.Scope
* @function
*
* @description
* Removes the current scope (and all of its children) from the parent scope. Removal implies
* that calls to {@link ng.$rootScope.Scope#$digest $digest()} will no longer
* propagate to the current scope and its children. Removal also implies that the current
* scope is eligible for garbage collection.
*
* The `$destroy()` is usually used by directives such as
* {@link ng.directive:ngRepeat ngRepeat} for managing the
* unrolling of the loop.
*
* Just before a scope is destroyed a `$destroy` event is broadcasted on this scope.
* Application code can register a `$destroy` event handler that will give it chance to
* perform any necessary cleanup.
*/
$destroy: function() {
// we can't destroy the root scope or a scope that has been already destroyed
if ($rootScope == this || this.$$destroyed) return;
var parent = this.$parent;
this.$broadcast('$destroy');
this.$$destroyed = true;
if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
// This is bogus code that works around Chrome's GC leak
// see: https://github.com/angular/angular.js/issues/1313#issuecomment-10378451
this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead =
this.$$childTail = null;
},
/**
* @ngdoc function
* @name ng.$rootScope.Scope#$eval
* @methodOf ng.$rootScope.Scope
* @function
*
* @description
* Executes the `expression` on the current scope returning the result. Any exceptions in the
* expression are propagated (uncaught). This is useful when evaluating Angular expressions.
*
* # Example
*
var scope = ng.$rootScope.Scope();
scope.a = 1;
scope.b = 2;
expect(scope.$eval('a+b')).toEqual(3);
expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3);
*
*
* @param {(string|function())=} expression An angular expression to be executed.
*
* - `string`: execute using the rules as defined in {@link guide/expression expression}.
* - `function(scope)`: execute the function with the current `scope` parameter.
*
* @returns {*} The result of evaluating the expression.
*/
$eval: function(expr, locals) {
return $parse(expr)(this, locals);
},
/**
* @ngdoc function
* @name ng.$rootScope.Scope#$evalAsync
* @methodOf ng.$rootScope.Scope
* @function
*
* @description
* Executes the expression on the current scope at a later point in time.
*
* The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only that:
*
* - it will execute in the current script execution context (before any DOM rendering).
* - at least one {@link ng.$rootScope.Scope#$digest $digest cycle} will be performed after
* `expression` execution.
*
* Any exceptions from the execution of the expression are forwarded to the
* {@link ng.$exceptionHandler $exceptionHandler} service.
*
* @param {(string|function())=} expression An angular expression to be executed.
*
* - `string`: execute using the rules as defined in {@link guide/expression expression}.
* - `function(scope)`: execute the function with the current `scope` parameter.
*
*/
$evalAsync: function(expr) {
this.$$asyncQueue.push(expr);
},
/**
* @ngdoc function
* @name ng.$rootScope.Scope#$apply
* @methodOf ng.$rootScope.Scope
* @function
*
* @description
* `$apply()` is used to execute an expression in angular from outside of the angular framework.
* (For example from browser DOM events, setTimeout, XHR or third party libraries).
* Because we are calling into the angular framework we need to perform proper scope life-cycle
* of {@link ng.$exceptionHandler exception handling},
* {@link ng.$rootScope.Scope#$digest executing watches}.
*
* ## Life cycle
*
* # Pseudo-Code of `$apply()`
*
function $apply(expr) {
try {
return $eval(expr);
} catch (e) {
$exceptionHandler(e);
} finally {
$root.$digest();
}
}
*
*
*
* Scope's `$apply()` method transitions through the following stages:
*
* 1. The {@link guide/expression expression} is executed using the
* {@link ng.$rootScope.Scope#$eval $eval()} method.
* 2. Any exceptions from the execution of the expression are forwarded to the
* {@link ng.$exceptionHandler $exceptionHandler} service.
* 3. The {@link ng.$rootScope.Scope#$watch watch} listeners are fired immediately after the expression
* was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method.
*
*
* @param {(string|function())=} exp An angular expression to be executed.
*
* - `string`: execute using the rules as defined in {@link guide/expression expression}.
* - `function(scope)`: execute the function with current `scope` parameter.
*
* @returns {*} The result of evaluating the expression.
*/
$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;
}
}
},
/**
* @ngdoc function
* @name ng.$rootScope.Scope#$on
* @methodOf ng.$rootScope.Scope
* @function
*
* @description
* Listens on events of a given type. See {@link ng.$rootScope.Scope#$emit $emit} for discussion of
* event life cycle.
*
* The event listener function format is: `function(event, args...)`. The `event` object
* passed into the listener has the following attributes:
*
* - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or `$broadcast`-ed.
* - `currentScope` - `{Scope}`: the current scope which is handling the event.
* - `name` - `{string}`: Name of the event.
* - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel further event
* propagation (available only for events that were `$emit`-ed).
* - `preventDefault` - `{function}`: calling `preventDefault` sets `defaultPrevented` flag to true.
* - `defaultPrevented` - `{boolean}`: true if `preventDefault` was called.
*
* @param {string} name Event name to listen on.
* @param {function(event, args...)} listener Function to call when the event is emitted.
* @returns {function()} Returns a deregistration function for this listener.
*/
$on: function(name, listener) {
var namedListeners = this.$$listeners[name];
if (!namedListeners) {
this.$$listeners[name] = namedListeners = [];
}
namedListeners.push(listener);
return function() {
namedListeners[indexOf(namedListeners, listener)] = null;
};
},
/**
* @ngdoc function
* @name ng.$rootScope.Scope#$emit
* @methodOf ng.$rootScope.Scope
* @function
*
* @description
* Dispatches an event `name` upwards through the scope hierarchy notifying the
* registered {@link ng.$rootScope.Scope#$on} listeners.
*
* The event life cycle starts at the scope on which `$emit` was called. All
* {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get notified.
* Afterwards, the event traverses upwards toward the root scope and calls all registered
* listeners along the way. The event will stop propagating if one of the listeners cancels it.
*
* Any exception emmited from the {@link ng.$rootScope.Scope#$on listeners} will be passed
* onto the {@link ng.$exceptionHandler $exceptionHandler} service.
*
* @param {string} name Event name to emit.
* @param {...*} args Optional set of arguments which will be passed onto the event listeners.
* @return {Object} Event object, see {@link ng.$rootScope.Scope#$on}
*/
$emit: 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