diff options
| author | Misko Hevery | 2011-03-23 09:33:29 -0700 | 
|---|---|---|
| committer | Vojta Jina | 2011-08-02 01:00:03 +0200 | 
| commit | 8f0dcbab804180828d6859b1340c86cf161209fb (patch) | |
| tree | d13d47d47a1889cb7c96a87cecacd2e25307d51c /src/Scope.js | |
| parent | 1f4b417184ce53af15474de065400f8a686430c5 (diff) | |
| download | angular.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.js | 1269 | 
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;  } | 
