aboutsummaryrefslogtreecommitdiffstats
path: root/src/Scope.js
diff options
context:
space:
mode:
authorMisko Hevery2011-03-23 09:33:29 -0700
committerVojta Jina2011-08-02 01:00:03 +0200
commit8f0dcbab804180828d6859b1340c86cf161209fb (patch)
treed13d47d47a1889cb7c96a87cecacd2e25307d51c /src/Scope.js
parent1f4b417184ce53af15474de065400f8a686430c5 (diff)
downloadangular.js-8f0dcbab804180828d6859b1340c86cf161209fb.tar.bz2
feat(scope): new and improved scope implementation
- Speed improvements (about 4x on flush phase) - Memory improvements (uses no function closures) - Break $eval into $apply, $dispatch, $flush - Introduced $watch and $observe Breaks angular.equals() use === instead of == Breaks angular.scope() does not take parent as first argument Breaks scope.$watch() takes scope as first argument Breaks scope.$set(), scope.$get are removed Breaks scope.$config is removed Breaks $route.onChange callback has not "this" bounded
Diffstat (limited to 'src/Scope.js')
-rw-r--r--src/Scope.js1269
1 files changed, 760 insertions, 509 deletions
diff --git a/src/Scope.js b/src/Scope.js
index b9fab638..572e9760 100644
--- a/src/Scope.js
+++ b/src/Scope.js
@@ -1,537 +1,788 @@
'use strict';
-function getter(instance, path, unboundFn) {
- if (!path) return instance;
- var element = path.split('.');
- var key;
- var lastInstance = instance;
- var len = element.length;
- for ( var i = 0; i < len; i++) {
- key = element[i];
- if (!key.match(/^[\$\w][\$\w\d]*$/))
- throw "Expression '" + path + "' is not a valid expression for accessing variables.";
- if (instance) {
- lastInstance = instance;
- instance = instance[key];
- }
- if (isUndefined(instance) && key.charAt(0) == '$') {
- var type = angular['Global']['typeOf'](lastInstance);
- type = angular[type.charAt(0).toUpperCase()+type.substring(1)];
- var fn = type ? type[[key.substring(1)]] : undefined;
- if (fn) {
- instance = bind(lastInstance, fn, lastInstance);
- return instance;
- }
- }
- }
- if (!unboundFn && isFunction(instance)) {
- return bind(lastInstance, instance);
- }
- return instance;
-}
-
-function setter(instance, path, value){
- var element = path.split('.');
- for ( var i = 0; element.length > 1; i++) {
- var key = element.shift();
- var newInstance = instance[key];
- if (!newInstance) {
- newInstance = {};
- instance[key] = newInstance;
- }
- instance = newInstance;
- }
- instance[element.shift()] = value;
- return value;
-}
-
-///////////////////////////////////
-var scopeId = 0,
- getterFnCache = {},
- compileCache = {},
- JS_KEYWORDS = {};
-forEach(
- ("abstract,boolean,break,byte,case,catch,char,class,const,continue,debugger,default," +
- "delete,do,double,else,enum,export,extends,false,final,finally,float,for,function,goto," +
- "if,implements,import,ininstanceof,intinterface,long,native,new,null,package,private," +
- "protected,public,return,short,static,super,switch,synchronized,this,throw,throws," +
- "transient,true,try,typeof,var,volatile,void,undefined,while,with").split(/,/),
- function(key){ JS_KEYWORDS[key] = true;}
-);
-function getterFn(path){
- var fn = getterFnCache[path];
- if (fn) return fn;
-
- var code = 'var l, fn, t;\n';
- forEach(path.split('.'), function(key) {
- key = (JS_KEYWORDS[key]) ? '["' + key + '"]' : '.' + key;
- code += 'if(!s) return s;\n' +
- 'l=s;\n' +
- 's=s' + key + ';\n' +
- 'if(typeof s=="function" && !(s instanceof RegExp)) s = function(){ return l'+key+'.apply(l, arguments); };\n';
- if (key.charAt(1) == '$') {
- // special code for super-imposed functions
- var name = key.substr(2);
- code += 'if(!s) {\n' +
- ' t = angular.Global.typeOf(l);\n' +
- ' fn = (angular[t.charAt(0).toUpperCase() + t.substring(1)]||{})["' + name + '"];\n' +
- ' if (fn) s = function(){ return fn.apply(l, [l].concat(Array.prototype.slice.call(arguments, 0, arguments.length))); };\n' +
- '}\n';
- }
- });
- code += 'return s;';
- fn = Function('s', code);
- fn["toString"] = function(){ return code; };
-
- return getterFnCache[path] = fn;
-}
+/**
+ * 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 expressionCompile(exp){
- if (typeof exp === $function) return exp;
- var fn = compileCache[exp];
- if (!fn) {
- var p = parser(exp);
- var fnSelf = p.statements();
- fn = compileCache[exp] = extend(
- function(){ return fnSelf(this);},
- {fnSelf: fnSelf});
- }
- return fn;
-}
-function errorHandlerFor(element, error) {
- elementError(element, NG_EXCEPTION, isDefined(error) ? formatError(error) : error);
-}
+function createScope(providers, instanceCache) {
+ var scope = new Scope();
+ (scope.$service = createInjector(scope, providers, instanceCache)).eager();
+ return scope;
+};
/**
* @workInProgress
- * @ngdoc overview
+ * @ngdoc function
* @name angular.scope
*
* @description
- * Scope is a JavaScript object and the execution context for expressions. You can think about
- * scopes as JavaScript objects that have extra APIs for registering watchers. A scope is the
- * context in which model (from the model-view-controller design pattern) exists.
+ * 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.
+ * <pre>
+ 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!');
+ * </pre>
+ *
+ * # Inheritance
+ * A scope can inherit from a parent scope, as in this example:
+ * <pre>
+ 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');
+ * </pre>
*
- * Angular scope objects provide the following methods:
+ * # Dependency Injection
+ * See {@link guide/dev_guide.di dependency injection}.
+ *
+ *
+ * @param {Object.<string, function()>=} providers Map of service factory which need to be provided
+ * for the current scope. Defaults to {@link angular.service}.
+ * @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.$$observers =
+ this.$$nextSibling = this.$$childHead = this.$$childTail = null;
+ this['this'] = this.$root = this;
+}
+
+/**
+ * @workInProgress
+ * @ngdoc property
+ * @name angular.scope.$id
+ * @returns {number} Unique scope ID (monotonically increasing alphanumeric sequence) useful for
+ * debugging.
+ */
+
+/**
+ * @workInProgress
+ * @ngdoc property
+ * @name angular.scope.$service
+ * @function
*
- * * {@link angular.scope.$become $become()} -
- * * {@link angular.scope.$bind $bind()} -
- * * {@link angular.scope.$eval $eval()} -
- * * {@link angular.scope.$get $get()} -
- * * {@link angular.scope.$new $new()} -
- * * {@link angular.scope.$onEval $onEval()} -
- * * {@link angular.scope.$service $service()} -
- * * {@link angular.scope.$set $set()} -
- * * {@link angular.scope.$tryEval $tryEval()} -
- * * {@link angular.scope.$watch $watch()} -
+ * @description
+ * Provides reference to an instance of {@link angular.injector injector} which can be used to
+ * retrieve {@link angular.service services}. In general the use of this api is discouraged,
+ * in favor of proper {@link guide/dev_guide.di dependency injection}.
*
- * For more information about how angular scope objects work, see {@link guide/dev_guide.scopes
- * Angular Scope Objects} in the angular Developer Guide.
+ * @returns {function} {@link angular.injector injector}
+ */
+
+/**
+ * @workInProgress
+ * @ngdoc property
+ * @name angular.scope.$root
+ * @returns {Scope} The root scope of the current scope hierarchy.
*/
-function createScope(parent, providers, instanceCache) {
- function Parent(){}
- parent = Parent.prototype = (parent || {});
- var instance = new Parent();
- var evalLists = {sorted:[]};
- var $log, $exceptionHandler;
-
- extend(instance, {
- 'this': instance,
- $id: (scopeId++),
- $parent: parent,
-
- /**
- * @workInProgress
- * @ngdoc function
- * @name angular.scope.$bind
- * @function
- *
- * @description
- * Binds a function `fn` to the current scope. See: {@link angular.bind}.
-
- <pre>
- var scope = angular.scope();
- var fn = scope.$bind(function(){
- return this;
- });
- expect(fn()).toEqual(scope);
- </pre>
- *
- * @param {function()} fn Function to be bound.
- */
- $bind: bind(instance, bind, instance),
-
-
- /**
- * @workInProgress
- * @ngdoc function
- * @name angular.scope.$get
- * @function
- *
- * @description
- * Returns the value for `property_chain` on the current scope. Unlike in JavaScript, if there
- * are any `undefined` intermediary properties, `undefined` is returned instead of throwing an
- * exception.
- *
- <pre>
- var scope = angular.scope();
- expect(scope.$get('person.name')).toEqual(undefined);
- scope.person = {};
- expect(scope.$get('person.name')).toEqual(undefined);
- scope.person.name = 'misko';
- expect(scope.$get('person.name')).toEqual('misko');
- </pre>
- *
- * @param {string} property_chain String representing name of a scope property. Optionally
- * properties can be chained with `.` (dot), e.g. `'person.name.first'`
- * @returns {*} Value for the (nested) property.
- */
- $get: bind(instance, getter, instance),
-
-
- /**
- * @workInProgress
- * @ngdoc function
- * @name angular.scope.$set
- * @function
- *
- * @description
- * Assigns a value to a property of the current scope specified via `property_chain`. Unlike in
- * JavaScript, if there are any `undefined` intermediary properties, empty objects are created
- * and assigned to them instead of throwing an exception.
- *
- <pre>
- var scope = angular.scope();
- expect(scope.person).toEqual(undefined);
- scope.$set('person.name', 'misko');
- expect(scope.person).toEqual({name:'misko'});
- expect(scope.person.name).toEqual('misko');
- </pre>
- *
- * @param {string} property_chain String representing name of a scope property. Optionally
- * properties can be chained with `.` (dot), e.g. `'person.name.first'`
- * @param {*} value Value to assign to the scope property.
- */
- $set: bind(instance, setter, instance),
-
-
- /**
- * @workInProgress
- * @ngdoc function
- * @name angular.scope.$eval
- * @function
- *
- * @description
- * Without the `exp` parameter triggers an eval cycle for this scope and its child scopes.
- *
- * With the `exp` parameter, compiles the expression to a function and calls it with `this` set
- * to the current scope and returns the result. In other words, evaluates `exp` as angular
- * expression in the context of the current scope.
- *
- * # Example
- <pre>
- var scope = angular.scope();
- scope.a = 1;
- scope.b = 2;
-
- expect(scope.$eval('a+b')).toEqual(3);
- expect(scope.$eval(function(){ return this.a + this.b; })).toEqual(3);
-
- scope.$onEval('sum = a+b');
- expect(scope.sum).toEqual(undefined);
- scope.$eval();
- expect(scope.sum).toEqual(3);
- </pre>
- *
- * @param {(string|function())=} exp An angular expression to be compiled to a function or a js
- * function.
- *
- * @returns {*} The result of calling compiled `exp` with `this` set to the current scope.
- */
- $eval: function(exp) {
- var type = typeof exp;
- var i, iSize;
- var j, jSize;
- var queue;
- var fn;
- if (type == $undefined) {
- for ( i = 0, iSize = evalLists.sorted.length; i < iSize; i++) {
- for ( queue = evalLists.sorted[i],
- jSize = queue.length,
- j= 0; j < jSize; j++) {
- instance.$tryEval(queue[j].fn, queue[j].handler);
+
+/**
+ * @workInProgress
+ * @ngdoc property
+ * @name angular.scope.$parent
+ * @returns {Scope} The parent scope of the current scope.
+ */
+
+
+Scope.prototype = {
+ /**
+ * @workInProgress
+ * @ngdoc function
+ * @name angular.scope.$new
+ * @function
+ *
+ * @description
+ * Creates a new child {@link angular.scope scope}. The new scope can optionally behave as a
+ * controller. The parent scope will propagate the {@link angular.scope.$digest $digest()} and
+ * {@link angular.scope.$flush $flush()} events. The scope can be removed from the scope
+ * hierarchy using {@link angular.scope.$destroy $destroy()}.
+ *
+ * {@link angular.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 {function()=} constructor Constructor function which the scope should behave as.
+ * @param {curryArguments=} ... Any additional arguments which are curried into the constructor.
+ * See {@link guide/dev_guide.di dependency injection}.
+ * @returns {Object} The newly created child scope.
+ *
+ */
+ $new: function(Class, curryArguments) {
+ var Child = function() {}; // should be anonymous; This is so that when the minifier munges
+ // the name it does not become random set of chars. These will then show up as class
+ // name in the debugger.
+ var child;
+ Child.prototype = this;
+ child = new Child();
+ child['this'] = child;
+ child.$parent = this;
+ child.$id = nextUid();
+ child.$$phase = child.$$watchers = child.$$observers =
+ child.$$nextSibling = child.$$childHead = child.$$childTail = null;
+ if (this.$$childHead) {
+ this.$$childTail.$$nextSibling = child;
+ this.$$childTail = child;
+ } else {
+ this.$$childHead = this.$$childTail = child;
+ }
+ // short circuit if we have no class
+ if (Class) {
+ // can't use forEach, we need speed!
+ var ClassPrototype = Class.prototype;
+ for(var key in ClassPrototype) {
+ child[key] = bind(child, ClassPrototype[key]);
+ }
+ this.$service.invoke(child, Class, curryArguments);
+ }
+ return child;
+ },
+
+ /**
+ * @workInProgress
+ * @ngdoc function
+ * @name angular.scope.$watch
+ * @function
+ *
+ * @description
+ * Registers a `listener` callback to be executed whenever the `watchExpression` changes.
+ *
+ * - The `watchExpression` is called on every call to {@link angular.scope.$digest $digest()} and
+ * should return the value which will be watched. (Since {@link angular.scope.$digest $digest()}
+ * reruns when it detects changes the `watchExpression` can execute multiple times per
+ * {@link angular.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. The inequality is determined according to
+ * {@link angular.equals} function. To save the value of the object for later comparison
+ * {@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 achieving my rerunning the watchers until no changes are detected. The rerun iteration
+ * limit is 100 to prevent infinity loop deadlock.
+ *
+ * # When to use `$watch`?
+ *
+ * The `$watch` should be used from within controllers to listen on properties *immediately* after
+ * a stimulus is applied to the system (see {@link angular.scope.$apply $apply()}). This is in
+ * contrast to {@link angular.scope.$observe $observe()} which is used from within the directives
+ * and which gets applied at some later point in time. In addition
+ * {@link angular.scope.$observe $observe()} must not modify the model.
+ *
+ * If you want to be notified whenever {@link angular.scope.$digest $digest} is called,
+ * you can register an `watchExpression` function with no `listener`. (Since `watchExpression`,
+ * can execute multiple times per {@link angular.scope.$digest $digest} cycle when a change is
+ * detected, be prepared for multiple calls to your listener.)
+ *
+ * # `$watch` vs `$observe`
+ *
+ * <table class="table">
+ * <tr>
+ * <th></td>
+ * <th>{@link angular.scope.$watch $watch()}</th>
+ * <th>{@link angular.scope.$observe $observe()}</th>
+ * </tr>
+ * <tr><th colspan="3" class="section">When to use it?</th></tr>
+ * <tr>
+ * <th>Purpose</th>
+ * <td>Application behavior (including further model mutation) in response to a model
+ * mutation.</td>
+ * <td>Update the DOM in response to a model mutation.</td>
+ * </tr>
+ * <tr>
+ * <th>Used from</th>
+ * <td>{@link angular.directive.ng:controller controller}</td>
+ * <td>{@link angular.directive directives}</td>
+ * </tr>
+ * <tr><th colspan="3" class="section">What fires listeners?</th></tr>
+ * <tr>
+ * <th>Directly</th>
+ * <td>{@link angular.scope.$digest $digest()}</td>
+ * <td>{@link angular.scope.$flush $flush()}</td>
+ * </tr>
+ * <tr>
+ * <th>Indirectly via {@link angular.scope.$apply $apply()}</th>
+ * <td>{@link angular.scope.$apply $apply} calls
+ * {@link angular.scope.$digest $digest()} after apply argument executes.</td>
+ * <td>{@link angular.scope.$apply $apply} schedules
+ * {@link angular.scope.$flush $flush()} at some future time via
+ * {@link angular.service.$updateView $updateView}</td>
+ * </tr>
+ * <tr><th colspan="3" class="section">API contract</th></tr>
+ * <tr>
+ * <th>Model mutation</th>
+ * <td>allowed: detecting mutations requires one or mare calls to `watchExpression' per
+ * {@link angular.scope.$digest $digest()} cycle</td>
+ * <td>not allowed: called once per {@link angular.scope.$flush $flush()} must be
+ * {@link http://en.wikipedia.org/wiki/Idempotence idempotent}
+ * (function without side-effects which can be called multiple times.)</td>
+ * </tr>
+ * <tr>
+ * <th>Initial Value</th>
+ * <td>uses the current value of `watchExpression` as the initial value. Does not fire on
+ * initial call to {@link angular.scope.$digest $digest()}, unless `watchExpression` has
+ * changed form the initial value.</td>
+ * <td>fires on first run of {@link angular.scope.$flush $flush()} regardless of value of
+ * `observeExpression`</td>
+ * </tr>
+ * </table>
+ *
+ *
+ *
+ * # Example
+ <pre>
+ 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);
+ </pre>
+ *
+ *
+ *
+ * @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.
+ * @returns {function()} a function which will call the `listener` with apprariate arguments.
+ * Useful for forcing initialization of listener.
+ */
+ $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: copy(get(scope)),
+ get: get
+ });
+ // we only return the initialization function for $watch (not for $observe), since creating
+ // function cost time and memory, and $observe functions do not need it.
+ return function() {
+ var value = get(scope);
+ listenFn(scope, value, value);
+ };
+ },
+
+ /**
+ * @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
+ <pre>
+ var scope = angular.scope();
+ scope.name = 'misko';
+ scope.counter = 0;
+
+ expect(scope.counter).toEqual(0);
+ scope.$flush('name', function(scope, newValue, oldValue) { counter = counter + 1; });
+ expect(scope.counter).toEqual(0);
+
+ scope.$flush();
+ // no variable change
+ expect(scope.counter).toEqual(0);
+
+ scope.name = 'adam';
+ scope.$flush();
+ expect(scope.counter).toEqual(1);
+ </pre>
+ *
+ * @returns {number} number of {@link angular.scope.$watch listeners} which fired.
+ *
+ */
+ $digest: function() {
+ var child,
+ watch, value, last,
+ watchers = this.$$watchers,
+ length, count = 0,
+ iterationCount, ttl = 100;
+
+ if (this.$$phase) {
+ throw Error(this.$$phase + ' already in progress');
+ }
+ this.$$phase = '$digest';
+ do {
+ iterationCount = 0;
+ if (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(this)) !== (last = watch.last) && !equals(value, last)) {
+ iterationCount++;
+ watch.fn(this, watch.last = copy(value), last);
+ }
+ } catch (e) {
+ this.$service('$exceptionHandler')(e);
}
}
- } else if (type === $function) {
- return exp.call(instance);
- } else if (type === 'string') {
- return expressionCompile(exp).call(instance);
}
- },
-
-
- /**
- * @workInProgress
- * @ngdoc function
- * @name angular.scope.$tryEval
- * @function
- *
- * @description
- * Evaluates the expression in the context of the current scope just like
- * {@link angular.scope.$eval} with expression parameter, but also wraps it in a try/catch
- * block.
- *
- * If an exception is thrown then `exceptionHandler` is used to handle the exception.
- *
- * # Example
- <pre>
- var scope = angular.scope();
- scope.error = function(){ throw 'myerror'; };
- scope.$exceptionHandler = function(e) {this.lastException = e; };
-
- expect(scope.$eval('error()'));
- expect(scope.lastException).toEqual('myerror');
- this.lastException = null;
-
- expect(scope.$eval('error()'), function(e) {this.lastException = e; });
- expect(scope.lastException).toEqual('myerror');
-
- var body = angular.element(window.document.body);
- expect(scope.$eval('error()'), body);
- expect(body.attr('ng-exception')).toEqual('"myerror"');
- expect(body.hasClass('ng-exception')).toEqual(true);
- </pre>
- *
- * @param {string|function()} expression Angular expression to evaluate.
- * @param {(function()|DOMElement)=} exceptionHandler Function to be called or DOMElement to be
- * decorated.
- * @returns {*} The result of `expression` evaluation.
- */
- $tryEval: function (expression, exceptionHandler) {
- var type = typeof expression;
- try {
- if (type == $function) {
- return expression.call(instance);
- } else if (type == 'string'){
- return expressionCompile(expression).call(instance);
- }
- } catch (e) {
- if ($log) $log.error(e);
- if (isFunction(exceptionHandler)) {
- exceptionHandler(e);
- } else if (exceptionHandler) {
- errorHandlerFor(exceptionHandler, e);
- } else if (isFunction($exceptionHandler)) {
- $exceptionHandler(e);
- }
+ child = this.$$childHead;
+ while(child) {
+ iterationCount += child.$digest();
+ child = child.$$nextSibling;
}
- },
-
-
- /**
- * @workInProgress
- * @ngdoc function
- * @name angular.scope.$watch
- * @function
- *
- * @description
- * Registers `listener` as a callback to be executed every time the `watchExp` changes. Be aware
- * that the callback gets, by default, called upon registration, this can be prevented via the
- * `initRun` parameter.
- *
- * # Example
- <pre>
- var scope = angular.scope();
- scope.name = 'misko';
- scope.counter = 0;
-
- expect(scope.counter).toEqual(0);
- scope.$watch('name', 'counter = counter + 1');
- expect(scope.counter).toEqual(1);
-
- scope.$eval();
- expect(scope.counter).toEqual(1);
-
- scope.name = 'adam';
- scope.$eval();
- expect(scope.counter).toEqual(2);
- </pre>
- *
- * @param {function()|string} watchExp Expression that should be evaluated and checked for
- * change during each eval cycle. Can be an angular string expression or a function.
- * @param {function()|string} listener Function (or angular string expression) that gets called
- * every time the value of the `watchExp` changes. The function will be called with two
- * parameters, `newValue` and `oldValue`.
- * @param {(function()|DOMElement)=} [exceptionHanlder=angular.service.$exceptionHandler] Handler
- * that gets called when `watchExp` or `listener` throws an exception. If a DOMElement is
- * specified as a handler, the element gets decorated by angular with the information about the
- * exception.
- * @param {boolean=} [initRun=true] Flag that prevents the first execution of the listener upon
- * registration.
- *
- */
- $watch: function(watchExp, listener, exceptionHandler, initRun) {
- var watch = expressionCompile(watchExp),
- last = watch.call(instance);
- listener = expressionCompile(listener);
- function watcher(firstRun){
- var value = watch.call(instance),
- // we have to save the value because listener can call ourselves => inf loop
- lastValue = last;
- if (firstRun || lastValue !== value) {
- last = value;
- instance.$tryEval(function(){
- return listener.call(instance, value, lastValue);
- }, exceptionHandler);
- }
+ count += iterationCount;
+ if(!(ttl--)) {
+ throw Error('100 $digest() iterations reached. Aborting!');
}
- instance.$onEval(PRIORITY_WATCH, watcher);
- if (isUndefined(initRun)) initRun = true;
- if (initRun) watcher(true);
- },
-
- /**
- * @workInProgress
- * @ngdoc function
- * @name angular.scope.$onEval
- * @function
- *
- * @description
- * Evaluates the `expr` expression in the context of the current scope during each
- * {@link angular.scope.$eval eval cycle}.
- *
- * # Example
- <pre>
- var scope = angular.scope();
- scope.counter = 0;
- scope.$onEval('counter = counter + 1');
- expect(scope.counter).toEqual(0);
- scope.$eval();
- expect(scope.counter).toEqual(1);
- </pre>
- *
- * @param {number} [priority=0] Execution priority. Lower priority numbers get executed first.
- * @param {string|function()} expr Angular expression or function to be executed.
- * @param {(function()|DOMElement)=} [exceptionHandler=angular.service.$exceptionHandler] Handler
- * function to call or DOM element to decorate when an exception occurs.
- *
- */
- $onEval: function(priority, expr, exceptionHandler){
- if (!isNumber(priority)) {
- exceptionHandler = expr;
- expr = priority;
- priority = 0;
+ } while (iterationCount);
+ this.$$phase = null;
+ return count;
+ },
+
+ /**
+ * @workInProgress
+ * @ngdoc function
+ * @name angular.scope.$observe
+ * @function
+ *
+ * @description
+ * Registers a `listener` callback to be executed during the {@link angular.scope.$flush $flush()}
+ * phase when the `observeExpression` changes..
+ *
+ * - The `observeExpression` is called on every call to {@link angular.scope.$flush $flush()} and
+ * should return the value which will be observed.
+ * - The `listener` is called only when the value from the current `observeExpression` and the
+ * previous call to `observeExpression' are not equal. The inequality is determined according to
+ * {@link angular.equals} function. To save the value of the object for later comparison
+ * {@link angular.copy} function is used. It also means that watching complex options will
+ * have adverse memory and performance implications.
+ *
+ * # When to use `$observe`?
+ *
+ * {@link angular.scope.$observe $observe()} is used from within directives and gets applied at
+ * some later point in time. Addition {@link angular.scope.$observe $observe()} must not
+ * modify the model. This is in contrast to {@link angular.scope.$watch $watch()} which should be
+ * used from within controllers to trigger a callback *immediately* after a stimulus is applied
+ * to the system (see {@link angular.scope.$apply $apply()}).
+ *
+ * If you want to be notified whenever {@link angular.scope.$flush $flush} is called,
+ * you can register an `observeExpression` function with no `listener`.
+ *
+ *
+ * # `$watch` vs `$observe`
+ *
+ * <table class="table">
+ * <tr>
+ * <th></td>
+ * <th>{@link angular.scope.$watch $watch()}</th>
+ * <th>{@link angular.scope.$observe $observe()}</th>
+ * </tr>
+ * <tr><th colspan="3" class="section">When to use it?</th></tr>
+ * <tr>
+ * <th>Purpose</th>
+ * <td>Application behavior (including further model mutation) in response to a model
+ * mutation.</td>
+ * <td>Update the DOM in response to a model mutation.</td>
+ * </tr>
+ * <tr>
+ * <th>Used from</th>
+ * <td>{@link angular.directive.ng:controller controller}</td>
+ * <td>{@link angular.directive directives}</td>
+ * </tr>
+ * <tr><th colspan="3" class="section">What fires listeners?</th></tr>
+ * <tr>
+ * <th>Directly</th>
+ * <td>{@link angular.scope.$digest $digest()}</td>
+ * <td>{@link angular.scope.$flush $flush()}</td>
+ * </tr>
+ * <tr>
+ * <th>Indirectly via {@link angular.scope.$apply $apply()}</th>
+ * <td>{@link angular.scope.$apply $apply} calls
+ * {@link angular.scope.$digest $digest()} after apply argument executes.</td>
+ * <td>{@link angular.scope.$apply $apply} schedules
+ * {@link angular.scope.$flush $flush()} at some future time via
+ * {@link angular.service.$updateView $updateView}</td>
+ * </tr>
+ * <tr><th colspan="3" class="section">API contract</th></tr>
+ * <tr>
+ * <th>Model mutation</th>
+ * <td>allowed: detecting mutations requires one or mare calls to `watchExpression' per
+ * {@link angular.scope.$digest $digest()} cycle</td>
+ * <td>not allowed: called once per {@link angular.scope.$flush $flush()} must be
+ * {@link http://en.wikipedia.org/wiki/Idempotence idempotent}
+ * (function without side-effects which can be called multiple times.)</td>
+ * </tr>
+ * <tr>
+ * <th>Initial Value</th>
+ * <td>uses the current value of `watchExpression` as the initial value. Does not fire on
+ * initial call to {@link angular.scope.$digest $digest()}, unless `watchExpression` has
+ * changed form the initial value.</td>
+ * <td>fires on first run of {@link angular.scope.$flush $flush()} regardless of value of
+ * `observeExpression`</td>
+ * </tr>
+ * </table>
+ *
+ * # Example
+ <pre>
+ var scope = angular.scope();
+ scope.name = 'misko';
+ scope.counter = 0;
+
+ expect(scope.counter).toEqual(0);
+ scope.$flush('name', function(scope, newValue, oldValue) { counter = counter + 1; });
+ expect(scope.counter).toEqual(0);
+
+ scope.$flush();
+ // no variable change
+ expect(scope.counter).toEqual(0);
+
+ scope.name = 'adam';
+ scope.$flush();
+ expect(scope.counter).toEqual(1);
+ </pre>
+ *
+ * @param {(function()|string)} observeExpression Expression that is evaluated on each
+ * {@link angular.scope.$flush $flush} 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 `observeExpression` 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.
+ */
+ $observe: function(watchExp, listener) {
+ var array = this.$$observers;
+
+ if (!array) {
+ array = this.$$observers = [];
+ }
+ // we use unshift since we use a while loop in $flush for speed.
+ // the while loop reads in reverse order.
+ array.unshift({
+ fn: compileToFn(listener || noop, 'listener'),
+ last: NaN,
+ get: compileToFn(watchExp, 'watch')
+ });
+ },
+
+ /**
+ * @workInProgress
+ * @ngdoc function
+ * @name angular.scope.$flush
+ * @function
+ *
+ * @description
+ * Process all of the {@link angular.scope.$observe observers} of the current scope
+ * and its children.
+ *
+ * Usually you don't call `$flush()` 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 scheduled a call to `$flush()` (with the
+ * help of the {@link angular.service.$updateView $updateView} service).
+ *
+ * If you want to be notified whenever `$flush()` is called,
+ * you can register a `observeExpression` function with {@link angular.scope.$observe $observe()}
+ * with no `listener`.
+ *
+ * You may have a need to call `$flush()` from within unit-tests, to simulate the scope
+ * life-cycle.
+ *
+ * # Example
+ <pre>
+ var scope = angular.scope();
+ scope.name = 'misko';
+ scope.counter = 0;
+
+ expect(scope.counter).toEqual(0);
+ scope.$flush('name', function(scope, newValue, oldValue) { counter = counter + 1; });
+ expect(scope.counter).toEqual(0);
+
+ scope.$flush();
+ // no variable change
+ expect(scope.counter).toEqual(0);
+
+ scope.name = 'adam';
+ scope.$flush();
+ expect(scope.counter).toEqual(1);
+ </pre>
+ *
+ */
+ $flush: function() {
+ var observers = this.$$observers,
+ child,
+ length,
+ observer, value, last;
+
+ if (this.$$phase) {
+ throw Error(this.$$phase + ' already in progress');
+ }
+ this.$$phase = '$flush';
+ if (observers) {
+ // process our watches
+ length = observers.length;
+ while (length--) {
+ try {
+ observer = observers[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 = observer.get(this)) !== (last = observer.last) && !equals(value, last)) {
+ observer.fn(this, observer.last = copy(value), last);
+ }
+ } catch (e){
+ this.$service('$exceptionHandler')(e);
+ }
}
- var evalList = evalLists[priority];
- if (!evalList) {
- evalList = evalLists[priority] = [];
- evalList.priority = priority;
- evalLists.sorted.push(evalList);
- evalLists.sorted.sort(function(a,b){return a.priority-b.priority;});
+ }
+ // observers can create new children
+ child = this.$$childHead;
+ while(child) {
+ child.$flush();
+ child = child.$$nextSibling;
+ }
+ this.$$phase = null;
+ },
+
+ /**
+ * @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()} and
+ * {@link angular.scope.$flush $flush()} 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;
+ var child = parent.$$childHead;
+ var lastChild = null;
+ var nextChild = null;
+ // We have to do a linear search, since we don't have doubly link list.
+ // But this is intentional since removals are rare, and doubly link list is not free.
+ while(child) {
+ if (child == this) {
+ nextChild = child.$$nextSibling;
+ if (parent.$$childHead == child) {
+ parent.$$childHead = nextChild;
+ }
+ if (lastChild) {
+ lastChild.$$nextSibling = nextChild;
+ }
+ if (parent.$$childTail == child) {
+ parent.$$childTail = lastChild;
+ }
+ return; // stop iterating we found it
+ } else {
+ lastChild = child;
+ child = child.$$nextSibling;
}
- evalList.push({
- fn: expressionCompile(expr),
- handler: exceptionHandler
- });
- },
-
- /**
- * @workInProgress
- * @ngdoc function
- * @name angular.scope.$become
- * @function
- * @deprecated This method will be removed before 1.0
- *
- * @description
- * Modifies the scope to act like an instance of the given class by:
- *
- * - copying the class's prototype methods
- * - applying the class's initialization function to the scope instance (without using the new
- * operator)
- *
- * That makes the scope be a `this` for the given class's methods — effectively an instance of
- * the given class with additional (scope) stuff. A scope can later `$become` another class.
- *
- * `$become` gets used to make the current scope act like an instance of a controller class.
- * This allows for use of a controller class in two ways.
- *
- * - as an ordinary JavaScript class for standalone testing, instantiated using the new
- * operator, with no attached view.
- * - as a controller for an angular model stored in a scope, "instantiated" by
- * `scope.$become(ControllerClass)`.
- *
- * Either way, the controller's methods refer to the model variables like `this.name`. When
- * stored in a scope, the model supports data binding. When bound to a view, {{name}} in the
- * HTML template refers to the same variable.
- */
- $become: function(Class) {
- if (isFunction(Class)) {
- instance.constructor = Class;
- forEach(Class.prototype, function(fn, name){
- instance[name] = bind(instance, fn);
- });
- instance.$service.invoke(instance, Class, slice.call(arguments, 1, arguments.length));
-
- //TODO: backwards compatibility hack, remove when we don't depend on init methods
- if (isFunction(Class.prototype.init)) {
- instance.init();
+ }
+ },
+
+ /**
+ * @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
+ <pre>
+ 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);
+ </pre>
+ *
+ * @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)
+ ? parser(expr).statements()
+ : expr || noop;
+ return fn(this);
+ },
+
+ /**
+ * @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} and scheduling
+ * {@link angular.service.$updateView updating of the view} which in turn
+ * {@link angular.scope.$digest executes observers} to update the DOM.
+ *
+ * ## Life cycle
+ *
+ * # Pseudo-Code of `$apply()`
+ function $apply(expr) {
+ try {
+ return $eval(expr);
+ } catch (e) {
+ $exceptionHandler(e);
+ } finally {
+ $root.$digest();
+ $updateView();
}
}
- },
-
- /**
- * @workInProgress
- * @ngdoc function
- * @name angular.scope.$new
- * @function
- *
- * @description
- * Creates a new {@link angular.scope scope}, that:
- *
- * - is a child of the current scope
- * - will {@link angular.scope.$become $become} of type specified via `constructor`
- *
- * @param {function()} constructor Constructor function of the type the new scope should assume.
- * @returns {Object} The newly created child scope.
- *
- */
- $new: function(constructor) {
- var child = createScope(instance);
- child.$become.apply(instance, concat([constructor], arguments, 1));
- instance.$onEval(child.$eval);
- return child;
+ *
+ *
+ * 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.
+ * 4. A DOM update is scheduled using the {@link angular.service.$updateView $updateView} service.
+ * The `$updateView` may merge multiple update requests into a single update, if the requests
+ * are issued in close time proximity.
+ * 6. The {@link angular.service.$updateView $updateView} service then fires DOM
+ * {@link angular.scope.$observe observers} using the {@link angular.scope.$flush $flush()}
+ * 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();
+ this.$service('$updateView')();
}
-
- });
-
- if (!parent.$root) {
- instance.$root = instance;
- instance.$parent = instance;
-
- /**
- * @workInProgress
- * @ngdoc function
- * @name angular.scope.$service
- * @function
- *
- * @description
- * Provides access to angular's dependency injector and
- * {@link angular.service registered services}. In general the use of this api is discouraged,
- * except for tests and components that currently don't support dependency injection (widgets,
- * filters, etc).
- *
- * @param {string} serviceId String ID of the service to return.
- * @returns {*} Value, object or function returned by the service factory function if any.
- */
- (instance.$service = createInjector(instance, providers, instanceCache)).eager();
}
+};
- $log = instance.$service('$log');
- $exceptionHandler = instance.$service('$exceptionHandler');
-
- return instance;
+function compileToFn(exp, name) {
+ var fn = isString(exp)
+ ? parser(exp).statements()
+ : exp;
+ assertArgFn(fn, name);
+ return fn;
}