'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. */ function createScope(providers, instanceCache) { var scope = new Scope(); (scope.$service = createInjector(scope, providers, instanceCache)).eager(); return scope; }; /** * @workInProgress * @ngdoc function * @name angular.scope * * @description * A root scope can be created by calling {@link angular.scope angular.scope()}. Child scopes * are created using the {@link angular.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. *
var scope = angular.scope();
scope.salutation = 'Hello';
scope.name = 'World';
expect(scope.greeting).toEqual(undefined);
scope.$watch('name', function(){
this.greeting = this.salutation + ' ' + this.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 = angular.scope();
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');
*
*
* # Dependency Injection
* See {@link guide/dev_guide.di dependency injection}.
*
*
* @param {Object.
var scope = angular.scope();
scope.name = 'misko';
scope.counter = 0;
expect(scope.counter).toEqual(0);
scope.$watch('name', function(scope, newValue, oldValue) { counter = 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 angular.scope.$digest $digest} cycle. A change in the return value triggers a
* call to the `listener`.
*
* - `string`: Evaluated as {@link guide/dev_guide.expressions 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/dev_guide.expressions expression}
* - `function(scope, newValue, oldValue)`: called with current `scope` an previous and
* current values as parameters.
*/
$watch: function(watchExp, listener) {
var scope = this;
var get = compileToFn(watchExp, 'watch');
var listenFn = compileToFn(listener || noop, 'listener');
var array = scope.$$watchers;
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({
fn: listenFn,
last: Number.NaN, // NaN !== NaN. We used this to force $watch to fire on first run.
get: get
});
},
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$digest
* @function
*
* @description
* Process all of the {@link angular.scope.$watch watchers} of the current scope and its children.
* Because a {@link angular.scope.$watch watcher}'s listener can change the model, the
* `$digest()` keeps calling the {@link angular.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 100.
*
* Usually you don't call `$digest()` directly in
* {@link angular.directive.ng:controller controllers} or in {@link angular.directive directives}.
* Instead a call to {@link angular.scope.$apply $apply()} (typically from within a
* {@link angular.directive directive}) will force a `$digest()`.
*
* If you want to be notified whenever `$digest()` is called,
* you can register a `watchExpression` function with {@link angular.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 = angular.scope();
scope.name = 'misko';
scope.counter = 0;
expect(scope.counter).toEqual(0);
scope.$digest('name', function(scope, newValue, oldValue) { counter = 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, next,
watchers,
asyncQueue,
length,
dirty, ttl = 100,
scope;
if (this.$$phase) {
throw Error(this.$$phase + ' already in progress');
}
do {
dirty = false;
scope = this;
do {
scope.$$phase = '$digest';
asyncQueue = scope.$$asyncQueue;
while(asyncQueue.length) {
try {
scope.$eval(asyncQueue.shift());
} catch (e) {
scope.$service('$exceptionHandler')(e);
}
}
if (watchers = scope.$$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(scope)) !== (last = watch.last) && !equals(value, last)) {
dirty = true;
watch.fn(scope, watch.last = copy(value), last);
}
} catch (e) {
scope.$service('$exceptionHandler')(e);
}
}
}
scope.$$phase = null;
// find the next scope in traversal.
if (!(next = scope.$$childHead || scope.$$nextSibling) && scope !== this) {
do {
scope = scope.$parent;
if (scope == this || (next = scope.$$nextSibling)) {
break;
}
} while (scope !== this);
}
} while (scope = next);
if(!(ttl--)) {
throw Error('100 $digest() iterations reached. Aborting!');
}
} while (dirty);
},
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$destroy
* @function
*
* @description
* Remove the current scope (and all of its children) from the parent scope. Removal implies
* that calls to {@link angular.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 angular.widget.@ng:repeat ng:repeat} for managing the unrolling of the loop.
*
*/
$destroy: function() {
if (this.$root == this) return; // we can't remove the root node;
var parent = this.$parent;
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;
},
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$eval
* @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 engular expressions.
*
* # Example
var scope = angular.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/dev_guide.expressions expression}.
* - `function(scope)`: execute the function with the current `scope` parameter.
*
* @returns {*} The result of evaluating the expression.
*/
$eval: function(expr) {
var fn = isString(expr)
? expressionCompile(expr)
: expr || noop;
return fn(this);
},
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$evalAsync
* @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 angular.scope.$digest $digest cycle} will be performed after
* `expression` execution.
*
* Any exceptions from the execution of the expression are forwarded to the
* {@link angular.service.$exceptionHandler $exceptionHandler} service.
*
* @param {(string|function())=} expression An angular expression to be executed.
*
* - `string`: execute using the rules as defined in {@link guide/dev_guide.expressions expression}.
* - `function(scope)`: execute the function with the current `scope` parameter.
*
*/
$evalAsync: function(expr) {
this.$$asyncQueue.push(expr);
},
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$apply
* @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 angular.service.$exceptionHandler exception handling},
* {@link angular.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/dev_guide.expressions expression} is executed using the
* {@link angular.scope.$eval $eval()} method.
* 2. Any exceptions from the execution of the expression are forwarded to the
* {@link angular.service.$exceptionHandler $exceptionHandler} service.
* 3. The {@link angular.scope.$watch watch} listeners are fired immediately after the expression
* was executed using the {@link angular.scope.$digest $digest()} method.
*
*
* @param {(string|function())=} exp An angular expression to be executed.
*
* - `string`: execute using the rules as defined in {@link guide/dev_guide.expressions expression}.
* - `function(scope)`: execute the function with current `scope` parameter.
*
* @returns {*} The result of evaluating the expression.
*/
$apply: function(expr) {
try {
return this.$eval(expr);
} catch (e) {
this.$service('$exceptionHandler')(e);
} finally {
this.$root.$digest();
}
}
};
function compileToFn(exp, name) {
var fn = isString(exp)
? expressionCompile(exp)
: exp;
assertArgFn(fn, name);
return fn;
}