mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-17 07:40:22 +00:00
1045 lines
40 KiB
JavaScript
1045 lines
40 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* DESIGN NOTES
|
|
*
|
|
* The design decisions behind the scope are 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 in terms of speed as well as memory:
|
|
* - No closures, instead use 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 beginning (shift) instead of at the end (push)
|
|
*
|
|
* Child scopes are created and removed often
|
|
* - Using an array would be slow since inserts in middle 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` iterations the scope should attempt to execute before giving up and
|
|
* assuming that the model is unstable.
|
|
*
|
|
* The current default is 10 iterations.
|
|
*
|
|
* In complex applications it's possible that the dependencies between `$watch`s will result in
|
|
* several digest iterations. However if an application needs more than the default 10 digest
|
|
* iterations for its model to stabilize then you should investigate what is causing the model to
|
|
* continuously change during the digest.
|
|
*
|
|
* Increasing the TTL could have performance implications, so you should not change it without
|
|
* proper justification.
|
|
*
|
|
* @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 descendant scopes of the root scope. Scopes provide separation
|
|
* between the model and the view, via a mechanism for watching the model for changes.
|
|
* They also provide an event emission/broadcast and subscription facility. See the
|
|
* {@link guide/scope developer guide on scopes}.
|
|
*/
|
|
function $RootScopeProvider(){
|
|
var TTL = 10;
|
|
var $rootScopeMinErr = minErr('$rootScope');
|
|
|
|
this.digestTtl = function(value) {
|
|
if (arguments.length) {
|
|
TTL = value;
|
|
}
|
|
return TTL;
|
|
};
|
|
|
|
this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser',
|
|
function( $injector, $exceptionHandler, $parse, $browser) {
|
|
|
|
/**
|
|
* @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#methods_$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.
|
|
* <pre>
|
|
* <file src="./test/ng/rootScopeSpec.js" tag="docs1" />
|
|
* </pre>
|
|
*
|
|
* # Inheritance
|
|
* A scope can inherit from a parent scope, as in this example:
|
|
* <pre>
|
|
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');
|
|
* </pre>
|
|
*
|
|
*
|
|
* @param {Object.<string, function()>=} providers Map of service factory which need to be
|
|
* provided for the current scope. Defaults to {@link ng}.
|
|
* @param {Object.<string, *>=} instanceCache Provides pre-instantiated services which should
|
|
* append/override services provided by `providers`. This is handy
|
|
* when unit-testing and having the need to override a default
|
|
* service.
|
|
* @returns {Object} Newly created scope.
|
|
*
|
|
*/
|
|
function Scope() {
|
|
this.$id = nextUid();
|
|
this.$$phase = this.$parent = this.$$watchers =
|
|
this.$$nextSibling = this.$$prevSibling =
|
|
this.$$childHead = this.$$childTail = null;
|
|
this['this'] = this.$root = this;
|
|
this.$$destroyed = false;
|
|
this.$$asyncQueue = [];
|
|
this.$$postDigestQueue = [];
|
|
this.$$listeners = {};
|
|
this.$$isolateBindings = {};
|
|
}
|
|
|
|
/**
|
|
* @ngdoc property
|
|
* @name ng.$rootScope.Scope#$id
|
|
* @propertyOf ng.$rootScope.Scope
|
|
* @returns {number} Unique scope ID (monotonically increasing alphanumeric sequence) useful for
|
|
* debugging.
|
|
*/
|
|
|
|
|
|
Scope.prototype = {
|
|
constructor: Scope,
|
|
/**
|
|
* @ngdoc function
|
|
* @name ng.$rootScope.Scope#$new
|
|
* @methodOf ng.$rootScope.Scope
|
|
* @function
|
|
*
|
|
* @description
|
|
* Creates a new child {@link ng.$rootScope.Scope scope}.
|
|
*
|
|
* The parent scope will propagate the {@link ng.$rootScope.Scope#$digest $digest()} and
|
|
* {@link ng.$rootScope.Scope#$digest $digest()} events. The scope can be removed from the
|
|
* scope hierarchy using {@link ng.$rootScope.Scope#$destroy $destroy()}.
|
|
*
|
|
* {@link ng.$rootScope.Scope#$destroy $destroy()} must be called on a scope when it is
|
|
* desired for the scope and its child scopes to be permanently detached from the parent and
|
|
* thus stop participating in model change detection and listener notification by invoking.
|
|
*
|
|
* @param {boolean} isolate If true, then the scope does not prototypically inherit from the
|
|
* parent scope. The scope is isolated, as it can not see parent scope properties.
|
|
* When creating widgets, it is useful for the widget to not accidentally read parent
|
|
* state.
|
|
*
|
|
* @returns {Object} The newly created child scope.
|
|
*
|
|
*/
|
|
$new: function(isolate) {
|
|
var ChildScope,
|
|
child;
|
|
|
|
if (isolate) {
|
|
child = new Scope();
|
|
child.$root = this.$root;
|
|
// ensure that there is just one async queue per $rootScope and its children
|
|
child.$$asyncQueue = this.$$asyncQueue;
|
|
child.$$postDigestQueue = this.$$postDigestQueue;
|
|
} else {
|
|
ChildScope = function() {}; // should be anonymous; This is so that when the minifier munges
|
|
// the name it does not become random set of chars. This will then show up as class
|
|
// name in the debugger.
|
|
ChildScope.prototype = this;
|
|
child = new ChildScope();
|
|
child.$id = nextUid();
|
|
}
|
|
child['this'] = child;
|
|
child.$$listeners = {};
|
|
child.$parent = this;
|
|
child.$$watchers = child.$$nextSibling = child.$$childHead = child.$$childTail = null;
|
|
child.$$prevSibling = this.$$childTail;
|
|
if (this.$$childHead) {
|
|
this.$$childTail.$$nextSibling = child;
|
|
this.$$childTail = child;
|
|
} else {
|
|
this.$$childHead = this.$$childTail = child;
|
|
}
|
|
return child;
|
|
},
|
|
|
|
/**
|
|
* @ngdoc function
|
|
* @name ng.$rootScope.Scope#$watch
|
|
* @methodOf ng.$rootScope.Scope
|
|
* @function
|
|
*
|
|
* @description
|
|
* Registers a `listener` callback to be executed whenever the `watchExpression` changes.
|
|
*
|
|
* - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest
|
|
* $digest()} and should return the value that will be watched. (Since
|
|
* {@link ng.$rootScope.Scope#$digest $digest()} reruns when it detects changes the
|
|
* `watchExpression` can execute multiple times per
|
|
* {@link ng.$rootScope.Scope#$digest $digest()} and should be idempotent.)
|
|
* - The `listener` is called only when the value from the current `watchExpression` and the
|
|
* previous call to `watchExpression` are not equal (with the exception of the initial run,
|
|
* see below). The inequality is determined according to
|
|
* {@link angular.equals} function. To save the value of the object for later comparison,
|
|
* the {@link angular.copy} function is used. It also means that watching complex options
|
|
* will have adverse memory and performance implications.
|
|
* - The watch `listener` may change the model, which may trigger other `listener`s to fire.
|
|
* This is achieved by rerunning the watchers until no changes are detected. The rerun
|
|
* iteration limit is 10 to prevent an infinite loop deadlock.
|
|
*
|
|
*
|
|
* If you want to be notified whenever {@link ng.$rootScope.Scope#$digest $digest} is called,
|
|
* you can register a `watchExpression` function with no `listener`. (Since `watchExpression`
|
|
* can execute multiple times per {@link ng.$rootScope.Scope#$digest $digest} cycle when a
|
|
* change is detected, be prepared for multiple calls to your listener.)
|
|
*
|
|
* After a watcher is registered with the scope, the `listener` fn is called asynchronously
|
|
* (via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the
|
|
* watcher. In rare cases, this is undesirable because the listener is called when the result
|
|
* of `watchExpression` didn't change. To detect this scenario within the `listener` fn, you
|
|
* can compare the `newVal` and `oldVal`. If these two values are identical (`===`) then the
|
|
* listener was called due to initialization.
|
|
*
|
|
* The example below contains an illustration of using a function as your $watch listener
|
|
*
|
|
*
|
|
* # Example
|
|
* <pre>
|
|
// 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);
|
|
|
|
|
|
|
|
// Using a listener function
|
|
var food;
|
|
scope.foodCounter = 0;
|
|
expect(scope.foodCounter).toEqual(0);
|
|
scope.$watch(
|
|
// This is the listener function
|
|
function() { return food; },
|
|
// This is the change handler
|
|
function(newValue, oldValue) {
|
|
if ( newValue !== oldValue ) {
|
|
// Only increment the counter if the value changed
|
|
scope.foodCounter = scope.foodCounter + 1;
|
|
}
|
|
}
|
|
);
|
|
// No digest has been run so the counter will be zero
|
|
expect(scope.foodCounter).toEqual(0);
|
|
|
|
// Run the digest but since food has not changed cout will still be zero
|
|
scope.$digest();
|
|
expect(scope.foodCounter).toEqual(0);
|
|
|
|
// Update food and run digest. Now the counter will increment
|
|
food = 'cheeseburger';
|
|
scope.$digest();
|
|
expect(scope.foodCounter).toEqual(1);
|
|
|
|
* </pre>
|
|
*
|
|
*
|
|
*
|
|
* @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, removing, and moving items belonging to an object or array.
|
|
*
|
|
*
|
|
* # Example
|
|
* <pre>
|
|
$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);
|
|
* </pre>
|
|
*
|
|
*
|
|
* @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, 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 (isArrayLike(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
|
|
* Processes 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#methods_directive directives}.
|
|
* Instead, you should call {@link ng.$rootScope.Scope#$apply $apply()} (typically from within
|
|
* a {@link ng.$compileProvider#methods_directive directives}), which 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`.
|
|
*
|
|
* In unit tests, you may need to call `$digest()` to simulate the scope life cycle.
|
|
*
|
|
* # Example
|
|
* <pre>
|
|
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);
|
|
* </pre>
|
|
*
|
|
*/
|
|
$digest: function() {
|
|
var watch, value, last,
|
|
watchers,
|
|
asyncQueue = this.$$asyncQueue,
|
|
postDigestQueue = this.$$postDigestQueue,
|
|
length,
|
|
dirty, ttl = TTL,
|
|
next, current, target = this,
|
|
watchLog = [],
|
|
logIdx, logMsg, asyncTask;
|
|
|
|
beginPhase('$digest');
|
|
|
|
do { // "while dirty" loop
|
|
dirty = false;
|
|
current = target;
|
|
|
|
while(asyncQueue.length) {
|
|
try {
|
|
asyncTask = asyncQueue.shift();
|
|
asyncTask.scope.$eval(asyncTask.expression);
|
|
} catch (e) {
|
|
clearPhase();
|
|
$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 (watch && (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) {
|
|
clearPhase();
|
|
$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 $rootScopeMinErr('infdig',
|
|
'{0} $digest() iterations reached. Aborting!\n' +
|
|
'Watchers fired in the last 5 iterations: {1}',
|
|
TTL, toJson(watchLog));
|
|
}
|
|
} while (dirty || asyncQueue.length);
|
|
|
|
clearPhase();
|
|
|
|
while(postDigestQueue.length) {
|
|
try {
|
|
postDigestQueue.shift()();
|
|
} catch (e) {
|
|
$exceptionHandler(e);
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
/**
|
|
* @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.
|
|
*
|
|
* Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to
|
|
* clean up DOM bindings before an element is removed from the DOM.
|
|
*/
|
|
|
|
/**
|
|
* @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 a chance to
|
|
* perform any necessary cleanup.
|
|
*
|
|
* Note that, in AngularJS, there is also a `$destroy` jQuery event, which can be used to
|
|
* clean up DOM bindings before an element is removed from the DOM.
|
|
*/
|
|
$destroy: function() {
|
|
// we can't destroy the root scope or a scope that has been already destroyed
|
|
if (this.$$destroyed) return;
|
|
var parent = this.$parent;
|
|
|
|
this.$broadcast('$destroy');
|
|
this.$$destroyed = true;
|
|
if (this === $rootScope) return;
|
|
|
|
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 and returns the result. Any exceptions in
|
|
* the expression are propagated (uncaught). This is useful when evaluating Angular
|
|
* expressions.
|
|
*
|
|
* # Example
|
|
* <pre>
|
|
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);
|
|
* </pre>
|
|
*
|
|
* @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.
|
|
*
|
|
* @param {(object)=} locals Local variables object, useful for overriding values in scope.
|
|
* @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 after the function that scheduled the evaluation (preferably before 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.
|
|
*
|
|
* __Note:__ if this function is called outside of a `$digest` cycle, a new `$digest` cycle
|
|
* will be scheduled. However, it is encouraged to always call code that changes the model
|
|
* from within an `$apply` call. That includes code evaluated via `$evalAsync`.
|
|
*
|
|
* @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) {
|
|
// if we are outside of an $digest loop and this is the first time we are scheduling async
|
|
// task also schedule async auto-flush
|
|
if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) {
|
|
$browser.defer(function() {
|
|
if ($rootScope.$$asyncQueue.length) {
|
|
$rootScope.$digest();
|
|
}
|
|
});
|
|
}
|
|
|
|
this.$$asyncQueue.push({scope: this, expression: expr});
|
|
},
|
|
|
|
$$postDigest : function(fn) {
|
|
this.$$postDigestQueue.push(fn);
|
|
},
|
|
|
|
/**
|
|
* @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()`
|
|
* <pre>
|
|
function $apply(expr) {
|
|
try {
|
|
return $eval(expr);
|
|
} catch (e) {
|
|
$exceptionHandler(e);
|
|
} finally {
|
|
$root.$digest();
|
|
}
|
|
}
|
|
* </pre>
|
|
*
|
|
*
|
|
* 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 emitted 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<length; i++) {
|
|
|
|
// if listeners were deregistered, defragment the array
|
|
if (!namedListeners[i]) {
|
|
namedListeners.splice(i, 1);
|
|
i--;
|
|
length--;
|
|
continue;
|
|
}
|
|
try {
|
|
//allow all listeners attached to the current scope to run
|
|
namedListeners[i].apply(null, listenerArgs);
|
|
} catch (e) {
|
|
$exceptionHandler(e);
|
|
}
|
|
}
|
|
//if any listener on the current scope stops propagation, prevent bubbling
|
|
if (stopPropagation) return event;
|
|
//traverse upwards
|
|
scope = scope.$parent;
|
|
} while (scope);
|
|
|
|
return event;
|
|
},
|
|
|
|
|
|
/**
|
|
* @ngdoc function
|
|
* @name ng.$rootScope.Scope#$broadcast
|
|
* @methodOf ng.$rootScope.Scope
|
|
* @function
|
|
*
|
|
* @description
|
|
* Dispatches an event `name` downwards to all child scopes (and their children) notifying the
|
|
* registered {@link ng.$rootScope.Scope#$on} listeners.
|
|
*
|
|
* The event life cycle starts at the scope on which `$broadcast` was called. All
|
|
* {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get
|
|
* notified. Afterwards, the event propagates to all direct and indirect scopes of the current
|
|
* scope and calls all registered listeners along the way. The event cannot be canceled.
|
|
*
|
|
* Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed
|
|
* onto the {@link ng.$exceptionHandler $exceptionHandler} service.
|
|
*
|
|
* @param {string} name Event name to broadcast.
|
|
* @param {...*} args Optional set of arguments which will be passed onto the event listeners.
|
|
* @return {Object} Event object, see {@link ng.$rootScope.Scope#$on}
|
|
*/
|
|
$broadcast: 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;
|
|
|
|
//down while you can, then up and next sibling or up and next sibling until back at root
|
|
do {
|
|
current = next;
|
|
event.currentScope = current;
|
|
listeners = current.$$listeners[name] || [];
|
|
for (i=0, length = listeners.length; i<length; i++) {
|
|
// if listeners were deregistered, defragment the array
|
|
if (!listeners[i]) {
|
|
listeners.splice(i, 1);
|
|
i--;
|
|
length--;
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
listeners[i].apply(null, listenerArgs);
|
|
} 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 $digest
|
|
if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) {
|
|
while(current !== target && !(next = current.$$nextSibling)) {
|
|
current = current.$parent;
|
|
}
|
|
}
|
|
} while ((current = next));
|
|
|
|
return event;
|
|
}
|
|
};
|
|
|
|
var $rootScope = new Scope();
|
|
|
|
return $rootScope;
|
|
|
|
|
|
function beginPhase(phase) {
|
|
if ($rootScope.$$phase) {
|
|
throw $rootScopeMinErr('inprog', '{0} already in progress', $rootScope.$$phase);
|
|
}
|
|
|
|
$rootScope.$$phase = phase;
|
|
}
|
|
|
|
function clearPhase() {
|
|
$rootScope.$$phase = null;
|
|
}
|
|
|
|
function compileToFn(exp, name) {
|
|
var fn = $parse(exp);
|
|
assertArgFn(fn, name);
|
|
return fn;
|
|
}
|
|
|
|
/**
|
|
* function used as an initial value for watchers.
|
|
* because it's unique we can easily tell it apart from other values
|
|
*/
|
|
function initWatchVal() {}
|
|
}];
|
|
}
|