mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-04-07 08:57:54 +08:00
Feedback team has often problems debugging inifinite digest errors, this change should reveal info about what watchers are causing the infinite loop
680 lines
24 KiB
JavaScript
680 lines
24 KiB
JavaScript
'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;
|
|
}
|