diff options
| author | Misko Hevery | 2011-10-25 10:30:27 -0700 |
|---|---|---|
| committer | Misko Hevery | 2011-11-14 16:39:31 -0800 |
| commit | 93b777c916ccff243c5a6080bf5f39860ac7bf39 (patch) | |
| tree | 65e70f8f767849506983060f52f8c864993777cd /src/service/scope.js | |
| parent | 5c70ff72e2d733e1f5deb72d296f118555b0019d (diff) | |
| download | angular.js-93b777c916ccff243c5a6080bf5f39860ac7bf39.tar.bz2 | |
move(scope): appease the History God
- renamed: src/Scope.js -> src/service/scope.js
- renamed: test/ScopeSpec.js -> test/service/scopeSpec.js
Diffstat (limited to 'src/service/scope.js')
| -rw-r--r-- | src/service/scope.js | 679 |
1 files changed, 679 insertions, 0 deletions
diff --git a/src/service/scope.js b/src/service/scope.js new file mode 100644 index 00000000..c4b9513b --- /dev/null +++ b/src/service/scope.js @@ -0,0 +1,679 @@ +'use strict'; + +/** + * DESIGN NOTES + * + * The design decisions behind the scope ware heavily favored for speed and memory consumption. + * + * The typical use of scope is to watch the expressions, which most of the time return the same + * value as last time so we optimize the operation. + * + * Closures construction is expensive from speed as well as memory: + * - no closures, instead ups prototypical inheritance for API + * - Internal state needs to be stored on scope directly, which means that private state is + * exposed as $$____ properties + * + * Loop operations are optimized by using while(count--) { ... } + * - this means that in order to keep the same order of execution as addition we have to add + * items to the array at the begging (shift) instead of at the end (push) + * + * Child scopes are created and removed often + * - Using array would be slow since inserts in meddle are expensive so we use linked list + * + * There are few watches then a lot of observers. This is why you don't want the observer to be + * implemented in the same way as watch. Watch requires return of initialization function which + * are expensive to construct. + */ + + +function createScope(providers, instanceCache) { + var scope = new Scope(); + (scope.$service = createInjector(scope, providers, instanceCache)).eager(); + return scope; +} + + +/** + * @ngdoc function + * @name angular.scope + * + * @description + * A root scope can be created by calling {@link angular.scope angular.scope()}. Child scopes + * are created using the {@link angular.scope.$new $new()} method. + * (Most scopes are created automatically when compiled HTML template is executed.) + * + * Here is a simple scope snippet to show how you can interact with the scope. + * <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> + * + * # 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.$$nextSibling = this.$$prevSibling = + this.$$childHead = this.$$childTail = null; + this.$destructor = noop; + this['this'] = this.$root = this; + this.$$asyncQueue = []; + this.$$listeners = {}; +} + +/** + * @ngdoc property + * @name angular.scope.$id + * @returns {number} Unique scope ID (monotonically increasing alphanumeric sequence) useful for + * debugging. + */ + +/** + * @ngdoc property + * @name angular.scope.$service + * @function + * + * @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}. + * + * @returns {function} {@link angular.injector injector} + */ + +/** + * @ngdoc property + * @name angular.scope.$root + * @returns {Scope} The root scope of the current scope hierarchy. + */ + +/** + * @ngdoc property + * @name angular.scope.$parent + * @returns {Scope} The parent scope of the current scope. + */ + + +Scope.prototype = { + /** + * @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.$digest $digest()} 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()=} Class Constructor function which the scope should be applied to the scope. + * @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.$$listeners = {}; + child.$parent = this; + child.$id = nextUid(); + child.$$asyncQueue = []; + child.$$phase = child.$$watchers = + child.$$nextSibling = child.$$childHead = child.$$childTail = null; + child.$$prevSibling = this.$$childTail; + if (this.$$childHead) { + this.$$childTail.$$nextSibling = child; + this.$$childTail = child; + } else { + this.$$childHead = this.$$childTail = child; + } + // 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; + }, + + /** + * @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 achieved by rerunning the watchers until no changes are detected. The rerun iteration + * limit is 100 to prevent infinity loop deadlock. + * + * + * 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.) + * + * + * # 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()} Returns a deregistration function for this listener. + */ + $watch: function(watchExp, listener) { + var scope = this, + get = compileToFn(watchExp, 'watch'), + listenFn = compileToFn(listener || noop, 'listener'), + array = scope.$$watchers, + watcher = { + fn: listenFn, + last: Number.NaN, // NaN !== NaN. We used this to force $watch to fire on first run. + get: get, + exp: watchExp + }; + + if (!array) { + array = scope.$$watchers = []; + } + // we use unshift since we use a while loop in $digest for speed. + // the while loop reads in reverse order. + array.unshift(watcher); + + return function() { + angularArray.remove(array, watcher); + }; + }, + + /** + * @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.$digest('name', function(scope, newValue, oldValue) { counter = counter + 1; }); + expect(scope.counter).toEqual(0); + + scope.$digest(); + // no variable change + expect(scope.counter).toEqual(0); + + scope.name = 'adam'; + scope.$digest(); + expect(scope.counter).toEqual(1); + </pre> + * + */ + $digest: function() { + var watch, value, last, + watchers, + asyncQueue, + length, + dirty, ttl = 100, + next, current, target = this, + watchLog = []; + + if (target.$$phase) { + throw Error(target.$$phase + ' already in progress'); + } + do { + + dirty = false; + current = target; + do { + current.$$phase = '$digest'; + asyncQueue = current.$$asyncQueue; + while(asyncQueue.length) { + try { + current.$eval(asyncQueue.shift()); + } catch (e) { + current.$service('$exceptionHandler')(e); + } + } + if ((watchers = current.$$watchers)) { + // process our watches + length = watchers.length; + while (length--) { + try { + watch = watchers[length]; + // Most common watches are on primitives, in which case we can short + // circuit it with === operator, only when === fails do we use .equals + if ((value = watch.get(current)) !== (last = watch.last) && !equals(value, last)) { + dirty = true; + watch.last = copy(value); + watch.fn(current, value, last); + if (ttl < 5) { + if (!watchLog[4-ttl]) watchLog[4-ttl] = []; + if (isFunction(watch.exp)) { + watchLog[4-ttl].push('fn: ' + (watch.exp.name || watch.exp.toString())); + } else { + watchLog[4-ttl].push(watch.exp); + } + } + } + } catch (e) { + current.$service('$exceptionHandler')(e); + } + } + } + + current.$$phase = null; + + // Insanity Warning: scope depth-first traversal + // yes, this code is a bit crazy, but it works and we have tests to prove it! + // this piece should be kept in sync with the traversal in $broadcast + if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { + while(current !== target && !(next = current.$$nextSibling)) { + current = current.$parent; + } + } + } while ((current = next)); + + if(!(ttl--)) { + throw Error('100 $digest() iterations reached. Aborting!\n' + + 'Watchers fired in the last 5 iterations: ' + toJson(watchLog)); + } + } while (dirty); + }, + + /** + * @ngdoc function + * @name angular.scope.$destroy + * @function + * + * @description + * Remove the current scope (and all of its children) from the parent scope. Removal implies + * that calls to {@link angular.scope.$digest $digest()} will no longer propagate to the current + * scope and its children. Removal also implies that the current scope is eligible for garbage + * collection. + * + * The destructing scope emits an `$destroy` {@link angular.scope.$emit event}. + * + * 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; + this.$emit('$destroy'); + var parent = this.$parent; + + if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; + if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; + if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; + if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; + }, + + /** + * @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) + ? expressionCompile(expr) + : expr || noop; + return fn(this); + }, + + /** + * @ngdoc function + * @name angular.scope.$evalAsync + * @function + * + * @description + * Executes the expression on the current scope at a later point in time. + * + * The `$evalAsync` makes no guarantees as to when the `expression` will be executed, only that: + * + * - it will execute in the current script execution context (before any DOM rendering). + * - at least one {@link angular.scope.$digest $digest cycle} will be performed after + * `expression` execution. + * + * Any exceptions from the execution of the expression are forwarded to the + * {@link angular.service.$exceptionHandler $exceptionHandler} service. + * + * @param {(string|function())=} expression An angular expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/dev_guide.expressions expression}. + * - `function(scope)`: execute the function with the current `scope` parameter. + * + */ + $evalAsync: function(expr) { + this.$$asyncQueue.push(expr); + }, + + /** + * @ngdoc function + * @name angular.scope.$apply + * @function + * + * @description + * `$apply()` is used to execute an expression in angular from outside of the angular framework. + * (For example from browser DOM events, setTimeout, XHR or third party libraries). + * Because we are calling into the angular framework we need to perform proper scope life-cycle + * of {@link angular.service.$exceptionHandler exception handling}, + * {@link angular.scope.$digest executing watches}. + * + * ## Life cycle + * + * # Pseudo-Code of `$apply()` + function $apply(expr) { + try { + return $eval(expr); + } catch (e) { + $exceptionHandler(e); + } finally { + $root.$digest(); + } + } + * + * + * Scope's `$apply()` method transitions through the following stages: + * + * 1. The {@link guide/dev_guide.expressions expression} is executed using the + * {@link angular.scope.$eval $eval()} method. + * 2. Any exceptions from the execution of the expression are forwarded to the + * {@link angular.service.$exceptionHandler $exceptionHandler} service. + * 3. The {@link angular.scope.$watch watch} listeners are fired immediately after the expression + * was executed using the {@link angular.scope.$digest $digest()} method. + * + * + * @param {(string|function())=} exp An angular expression to be executed. + * + * - `string`: execute using the rules as defined in {@link guide/dev_guide.expressions expression}. + * - `function(scope)`: execute the function with current `scope` parameter. + * + * @returns {*} The result of evaluating the expression. + */ + $apply: function(expr) { + try { + return this.$eval(expr); + } catch (e) { + this.$service('$exceptionHandler')(e); + } finally { + this.$root.$digest(); + } + }, + + /** + * @ngdoc function + * @name angular.scope.$on + * @function + * + * @description + * Listen on events of a given type. See {@link angular.scope.$emit $emit} for discussion of + * event life cycle. + * + * @param {string} name Event name to listen on. + * @param {function(event)} listener Function to call when the event is emitted. + * @returns {function()} Returns a deregistration function for this listener. + * + * The event listener function format is: `function(event)`. The `event` object passed into the + * listener has the following attributes + * - `targetScope` - {Scope}: the scope on which the event was `$emit`-ed or `$broadcast`-ed. + * - `currentScope` - {Scope}: the current scope which is handling the event. + * - `name` - {string}: Name of the event. + * - `cancel` - {function=}: calling `cancel` function will cancel further event propagation + * (available only for events that were `$emit`-ed). + */ + $on: function(name, listener) { + var namedListeners = this.$$listeners[name]; + if (!namedListeners) { + this.$$listeners[name] = namedListeners = []; + } + namedListeners.push(listener); + + return function() { + angularArray.remove(namedListeners, listener); + }; + }, + + + /** + * @ngdoc function + * @name angular.scope.$emit + * @function + * + * @description + * Dispatches an event `name` upwards through the scope hierarchy notifying the + * registered {@link angular.scope.$on} listeners. + * + * The event life cycle starts at the scope on which `$emit` was called. All + * {@link angular.scope.$on listeners} listening for `name` event on this scope get notified. + * Afterwards, the event traverses upwards toward the root scope and calls all registered + * listeners along the way. The event will stop propagating if one of the listeners cancels it. + * + * Any exception emmited from the {@link angular.scope.$on listeners} will be passed + * onto the {@link angular.service.$exceptionHandler $exceptionHandler} service. + * + * @param {string} name Event name to emit. + * @param {...*} args Optional set of arguments which will be passed onto the event listeners. + */ + $emit: function(name, args) { + var empty = [], + namedListeners, + canceled = false, + scope = this, + event = { + name: name, + targetScope: scope, + cancel: function() {canceled = true;} + }, + listenerArgs = concat([event], arguments, 1), + i, length; + + do { + namedListeners = scope.$$listeners[name] || empty; + event.currentScope = scope; + for (i=0, length=namedListeners.length; i<length; i++) { + try { + namedListeners[i].apply(null, listenerArgs); + if (canceled) return; + } catch (e) { + scope.$service('$exceptionHandler')(e); + } + } + //traverse upwards + scope = scope.$parent; + } while (scope); + }, + + + /** + * @ngdoc function + * @name angular.scope.$broadcast + * @function + * + * @description + * Dispatches an event `name` downwards to all child scopes (and their children) notifying the + * registered {@link angular.scope.$on} listeners. + * + * The event life cycle starts at the scope on which `$broadcast` was called. All + * {@link angular.scope.$on listeners} listening for `name` event on this scope get notified. + * Afterwards, the event propagates to all direct and indirect scopes of the current scope and + * calls all registered listeners along the way. The event cannot be canceled. + * + * Any exception emmited from the {@link angular.scope.$on listeners} will be passed + * onto the {@link angular.service.$exceptionHandler $exceptionHandler} service. + * + * @param {string} name Event name to emit. + * @param {...*} args Optional set of arguments which will be passed onto the event listeners. + */ + $broadcast: function(name, args) { + var target = this, + current = target, + next = target, + event = { name: name, + targetScope: target }, + listenerArgs = concat([event], arguments, 1); + + //down while you can, then up and next sibling or up and next sibling until back at root + do { + current = next; + event.currentScope = current; + forEach(current.$$listeners[name], function(listener) { + try { + listener.apply(null, listenerArgs); + } catch(e) { + current.$service('$exceptionHandler')(e); + } + }); + + // Insanity Warning: scope depth-first traversal + // yes, this code is a bit crazy, but it works and we have tests to prove it! + // this piece should be kept in sync with the traversal in $digest + if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { + while(current !== target && !(next = current.$$nextSibling)) { + current = current.$parent; + } + } + } while ((current = next)); + } +}; + + +function compileToFn(exp, name) { + var fn = isString(exp) + ? expressionCompile(exp) + : exp; + assertArgFn(fn, name); + return fn; +} |
