Scope class
Scope has two responsibilities. 1) to keep track af watches and 2) to keep references to the model so that they are available for data-binding.
@proxy class Scope implements Map { String $id; Scope $parent; Scope $root; num _nextId = 0; ExceptionHandler _exceptionHandler; Parser _parser; Zone _zone; num _ttl; String _phase; Map<String, Object> _properties = {}; List<Function> _innerAsyncQueue; List<Function> _outerAsyncQueue; List<_Watch> _watchers = []; Map<String, List<Function>> _listeners = {}; Scope _nextSibling, _prevSibling, _childHead, _childTail; bool _isolate = false; Profiler _perf; Scope(ExceptionHandler this._exceptionHandler, Parser this._parser, ScopeDigestTTL ttl, Zone this._zone, Profiler this._perf) { _properties[r'this']= this; _ttl = ttl.ttl; $root = this; $id = '_${$root._nextId++}'; _innerAsyncQueue = []; _outerAsyncQueue = []; // Set up the zone to auto digest this scope. _zone.onTurnDone = $digest; _zone.onError = (e, s, ls) => _exceptionHandler(e, s); } Scope._child(Scope this.$parent, bool this._isolate, Profiler this._perf) { _exceptionHandler = $parent._exceptionHandler; _parser = $parent._parser; _ttl = $parent._ttl; _properties[r'this'] = this; _zone = $parent._zone; $root = $parent.$root; $id = '_${$root._nextId++}'; _innerAsyncQueue = $parent._innerAsyncQueue; _outerAsyncQueue = $parent._outerAsyncQueue; _prevSibling = $parent._childTail; if ($parent._childHead != null) { $parent._childTail._nextSibling = this; $parent._childTail = this; } else { $parent._childHead = $parent._childTail = this; } } _identical(a, b) => identical(a, b) || (a is String && b is String && a == b) || (a is num && b is num && a.isNaN && b.isNaN); containsKey(String name) => this[name] != null; remove(String name) => this._properties.remove(name); operator []=(String name, value) => _properties[name] = value; operator [](String name) { if (name == r'$id') return this.$id; if (name == r'$parent') return this.$parent; if (name == r'$root') return this.$root; var scope = this; do { if (scope._properties.containsKey(name)) { return scope._properties[name]; } else if (!scope._isolate) { scope = scope.$parent; } else { return null; } } while(scope != null); return null; } noSuchMethod(Invocation invocation) { var name = MirrorSystem.getName(invocation.memberName); if (invocation.isGetter) { return this[name]; } else if (invocation.isSetter) { var value = invocation.positionalArguments[0]; name = name.substring(0, name.length - 1); this[name] = value; return value; } else { if (this[name] is Function) { return this[name](); } else { super.noSuchMethod(invocation); } } } $new([bool isolate = false]) { return new Scope._child(this, isolate, _perf); } $watch(watchExp, [Function listener, String watchStr]) { if (watchStr == null) { watchStr = watchExp.toString(); // Keep prod fast assert((() { watchStr = _source(watchExpr); return true; })()); } var watcher = new _Watch(_compileToFn(listener), _initWatchVal, _compileToFn(watchExp), watchStr); // we use unshift since we use a while loop in $digest for speed. // the while loop reads in reverse order. _watchers.insert(0, watcher); return () => _watchers.remove(watcher); } $watchCollection(obj, listener, [String expression]) { var oldValue; var newValue; num changeDetected = 0; Function objGetter = _compileToFn(obj); List internalArray = []; Map internalMap = {}; num oldLength = 0; var $watchCollectionWatch = (_) { newValue = objGetter(this); var newLength, key; if (newValue is! Map && newValue is! List) { if (!_identical(oldValue, newValue)) { oldValue = newValue; changeDetected++; } } else if (newValue is List) { if (!_identical(oldValue, internalArray)) { // we are transitioning from something which was not an array into array. oldValue = internalArray; oldLength = oldValue.length = 0; changeDetected++; } newLength = newValue.length; if (oldLength != newLength) { // if lengths do not match we need to trigger change notification changeDetected++; oldValue.length = oldLength = newLength; } // copy the items to oldValue and look for changes. for (var i = 0; i < newLength; i++) { if (!_identical(oldValue[i], newValue[i])) { changeDetected++; oldValue[i] = newValue[i]; } } } else { // Map if (!_identical(oldValue, internalMap)) { // we are transitioning from something which was not an object into object. oldValue = internalMap = {}; oldLength = 0; changeDetected++; } // copy the items to oldValue and look for changes. newLength = 0; newValue.forEach((key, value) { newLength++; if (oldValue.containsKey(key)) { if (!_identical(oldValue[key], value)) { changeDetected++; oldValue[key] = value; } } else { oldLength++; oldValue[key] = value; changeDetected++; } }); if (oldLength > newLength) { // we used to have more keys, need to find them and destroy them. changeDetected++; var keysToRemove = []; oldValue.forEach((key, _) { if (!newValue.containsKey(key)) { oldLength--; keysToRemove.add(key); } }); keysToRemove.forEach((k) { oldValue.remove(k); }); } } return changeDetected; }; var $watchCollectionAction = (_, __, ___) { relaxFnApply(listener, [newValue, oldValue, this]); }; return this.$watch($watchCollectionWatch, $watchCollectionAction, expression == null ? obj : expression); } /** * Add this function to your code if you want to add a $digest * and want to assert that the digest will be called on this turn. * This method will be deleted when we are comfortable with * auto-digesting scope. */ $$verifyDigestWillRun() { _zone.assertInTurn(); } $digest() { var innerAsyncQueue = _innerAsyncQueue, length, dirty, _ttlLeft = _ttl; List<List<String>> watchLog = []; List<_Watch> watchers; _Watch watch; Scope next, current, target = this; _beginPhase('\$digest'); try { do { // "while dirty" loop dirty = false; current = target; //asyncQueue = current._asyncQueue; //dump('aQ: ${asyncQueue.length}'); while(innerAsyncQueue.length > 0) { try { var workFn = innerAsyncQueue.removeAt(0); assert(_perf.startTimer('ng.innerAsync(${_source(workFn)})') != false); $root.$eval(workFn); } catch (e, s) { _exceptionHandler(e, s); } finally { assert(_perf.stopTimer('ng.innerAsync(${_source(workFn)})') != false); } } assert(_perf.startTimer('ng.dirty_check.${_ttl-_ttlLeft}') != false); do { // "traverse the scopes" loop if ((watchers = current._watchers) != null) { // process our watches length = watchers.length; while (length-- > 0) { try { watch = watchers[length]; var value = watch.get(current); var last = watch.last; if (!_identical(value, last)) { dirty = true; watch.last = value; assert(_perf.startTimer('ng.fire(${watch.exp})') != false); watch.fn(value, ((last == _initWatchVal) ? value : last), current); assert(_perf.stopTimer('ng.fire(${watch.exp})') != false); } } catch (e, s) { _exceptionHandler(e, s); } } } // 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 (current._childHead == null) { if (current == target) { next = null; } else { next = current._nextSibling; if (next == null) { while(current != target && (next = current._nextSibling) == null) { current = current.$parent; } } } } else { next = current._childHead; } } while ((current = next) != null); assert(_perf.stopTimer('ng.dirty_check.${_ttl-_ttlLeft}') != false); if(dirty && (_ttlLeft--) == 0) { throw '$_ttl \$digest() iterations reached. Aborting!\n' + 'Watchers fired in the last 5 iterations: ${_toJson(watchLog)}'; } } while (dirty || innerAsyncQueue.length > 0); while(_outerAsyncQueue.length > 0) { try { var workFn = _outerAsyncQueue.removeAt(0); assert(_perf.startTimer('ng.outerAsync(${_source(workFn)})') != false); $root.$eval(workFn); } catch (e, s) { _exceptionHandler(e, s); } finally { assert(_perf.stopTimer('ng.outerAsync(${_source(workFn)})') != false); } } } finally { _clearPhase(); } } $destroy() { if ($root == this) return; // we can't remove the root node; $broadcast(r'$destroy'); if ($parent._childHead == this) $parent._childHead = _nextSibling; if ($parent._childTail == this) $parent._childTail = _prevSibling; if (_prevSibling != null) _prevSibling._nextSibling = _nextSibling; if (_nextSibling != null) _nextSibling._prevSibling = _prevSibling; } $eval(expr, [locals]) { return relaxFnArgs(_compileToFn(expr))(this, locals); } $evalAsync(expr, {outsideDigest: false}) { if (outsideDigest) { _outerAsyncQueue.add(expr); } else { _innerAsyncQueue.add(expr); } } $apply([expr]) { return _zone.run(() { try { assert(_perf.startTimer('ng.\$apply(${_source(expr)})') != false); return $eval(expr); } catch (e, s) { _exceptionHandler(e, s); } finally { assert(_perf.stopTimer('ng.\$apply(${_source(expr)})') != false); } }); } $on(name, listener) { var namedListeners = _listeners[name]; if (!_listeners.containsKey(name)) { _listeners[name] = namedListeners = []; } namedListeners.add(listener); return () { namedListeners.remove(listener); }; } $emit(name, [List args]) { var empty = [], namedListeners, scope = this, event = new ScopeEvent(name, this), listenerArgs = [event], i; if (args != null) { listenerArgs.addAll(args); } do { namedListeners = scope._listeners[name]; if (namedListeners != null) { event.currentScope = scope; i = 0; for (var length = namedListeners.length; i<length; i++) { try { relaxFnApply(namedListeners[i], listenerArgs); if (event.propagationStopped) return event; } catch (e, s) { _exceptionHandler(e, s); } } } //traverse upwards scope = scope.$parent; } while (scope != null); return event; } $broadcast(String name, [List listenerArgs]) { var target = this, current = target, next = target, event = new ScopeEvent(name, this); //down while you can, then up and next sibling or up and next sibling until back at root if (listenerArgs == null) { listenerArgs = []; } listenerArgs.insert(0, event); do { current = next; event.currentScope = current; if (current._listeners.containsKey(name)) { current._listeners[name].forEach((listener) { try { relaxFnApply(listener, listenerArgs); } catch(e, s) { _exceptionHandler(e, s); } }); } // 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 (current._childHead == null) { if (current == target) { next = null; } else { next = current._nextSibling; if (next == null) { while(current != target && (next = current._nextSibling) == null) { current = current.$parent; } } } } else { next = current._childHead; } } while ((current = next) != null); return event; } _beginPhase(phase) { if ($root._phase != null) { // TODO(deboer): Remove the []s when dartbug.com/11999 is fixed. throw ['${$root._phase} already in progress']; } assert(_perf.startTimer('ng.phase.${phase}') != false); $root._phase = phase; } _clearPhase() { assert(_perf.stopTimer('ng.phase.${$root._phase}') != false); $root._phase = null; } Function _compileToFn(exp) { if (exp == null) { return () => null; } else if (exp is String) { return _parser(exp).eval; } else if (exp is Function) { return exp; } else { throw 'Expecting String or Function'; } } }
Implements
Constructors
new Scope(ExceptionHandler _exceptionHandler, Parser _parser, ScopeDigestTTL ttl, Zone _zone, Profiler _perf) #
Creates a Map instance with the default implementation.
Scope(ExceptionHandler this._exceptionHandler, Parser this._parser, ScopeDigestTTL ttl, Zone this._zone, Profiler this._perf) { _properties[r'this']= this; _ttl = ttl.ttl; $root = this; $id = '_${$root._nextId++}'; _innerAsyncQueue = []; _outerAsyncQueue = []; // Set up the zone to auto digest this scope. _zone.onTurnDone = $digest; _zone.onError = (e, s, ls) => _exceptionHandler(e, s); }
Properties
String $id #
String $id
final bool isEmpty #
Returns true if there is no {key, value} pair in the map.
bool get isEmpty;
final bool isNotEmpty #
Returns true if there is at least one {key, value} pair in the map.
bool get isNotEmpty;
Operators
dynamic operator [](String name) #
Returns the value for the given key
or null if key
is not
in the map. Because null values are supported, one should either
use containsKey to distinguish between an absent key and a null
value, or use the putIfAbsent
method.
operator [](String name) { if (name == r'$id') return this.$id; if (name == r'$parent') return this.$parent; if (name == r'$root') return this.$root; var scope = this; do { if (scope._properties.containsKey(name)) { return scope._properties[name]; } else if (!scope._isolate) { scope = scope.$parent; } else { return null; } } while(scope != null); return null; }
dynamic operator []=(String name, value) #
Associates the key
with the given
value.
operator []=(String name, value) => _properties[name] = value;
Methods
dynamic $$verifyDigestWillRun() #
Add this function to your code if you want to add a $digest and want to assert that the digest will be called on this turn. This method will be deleted when we are comfortable with auto-digesting scope.
$$verifyDigestWillRun() { _zone.assertInTurn(); }
dynamic $apply([expr]) #
$apply([expr]) { return _zone.run(() { try { assert(_perf.startTimer('ng.\$apply(${_source(expr)})') != false); return $eval(expr); } catch (e, s) { _exceptionHandler(e, s); } finally { assert(_perf.stopTimer('ng.\$apply(${_source(expr)})') != false); } }); }
dynamic $broadcast(String name, [List listenerArgs]) #
$broadcast(String name, [List listenerArgs]) { var target = this, current = target, next = target, event = new ScopeEvent(name, this); //down while you can, then up and next sibling or up and next sibling until back at root if (listenerArgs == null) { listenerArgs = []; } listenerArgs.insert(0, event); do { current = next; event.currentScope = current; if (current._listeners.containsKey(name)) { current._listeners[name].forEach((listener) { try { relaxFnApply(listener, listenerArgs); } catch(e, s) { _exceptionHandler(e, s); } }); } // 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 (current._childHead == null) { if (current == target) { next = null; } else { next = current._nextSibling; if (next == null) { while(current != target && (next = current._nextSibling) == null) { current = current.$parent; } } } } else { next = current._childHead; } } while ((current = next) != null); return event; }
dynamic $destroy() #
$destroy() { if ($root == this) return; // we can't remove the root node; $broadcast(r'$destroy'); if ($parent._childHead == this) $parent._childHead = _nextSibling; if ($parent._childTail == this) $parent._childTail = _prevSibling; if (_prevSibling != null) _prevSibling._nextSibling = _nextSibling; if (_nextSibling != null) _nextSibling._prevSibling = _prevSibling; }
dynamic $digest() #
$digest() { var innerAsyncQueue = _innerAsyncQueue, length, dirty, _ttlLeft = _ttl; List<List<String>> watchLog = []; List<_Watch> watchers; _Watch watch; Scope next, current, target = this; _beginPhase('\$digest'); try { do { // "while dirty" loop dirty = false; current = target; //asyncQueue = current._asyncQueue; //dump('aQ: ${asyncQueue.length}'); while(innerAsyncQueue.length > 0) { try { var workFn = innerAsyncQueue.removeAt(0); assert(_perf.startTimer('ng.innerAsync(${_source(workFn)})') != false); $root.$eval(workFn); } catch (e, s) { _exceptionHandler(e, s); } finally { assert(_perf.stopTimer('ng.innerAsync(${_source(workFn)})') != false); } } assert(_perf.startTimer('ng.dirty_check.${_ttl-_ttlLeft}') != false); do { // "traverse the scopes" loop if ((watchers = current._watchers) != null) { // process our watches length = watchers.length; while (length-- > 0) { try { watch = watchers[length]; var value = watch.get(current); var last = watch.last; if (!_identical(value, last)) { dirty = true; watch.last = value; assert(_perf.startTimer('ng.fire(${watch.exp})') != false); watch.fn(value, ((last == _initWatchVal) ? value : last), current); assert(_perf.stopTimer('ng.fire(${watch.exp})') != false); } } catch (e, s) { _exceptionHandler(e, s); } } } // 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 (current._childHead == null) { if (current == target) { next = null; } else { next = current._nextSibling; if (next == null) { while(current != target && (next = current._nextSibling) == null) { current = current.$parent; } } } } else { next = current._childHead; } } while ((current = next) != null); assert(_perf.stopTimer('ng.dirty_check.${_ttl-_ttlLeft}') != false); if(dirty && (_ttlLeft--) == 0) { throw '$_ttl \$digest() iterations reached. Aborting!\n' + 'Watchers fired in the last 5 iterations: ${_toJson(watchLog)}'; } } while (dirty || innerAsyncQueue.length > 0); while(_outerAsyncQueue.length > 0) { try { var workFn = _outerAsyncQueue.removeAt(0); assert(_perf.startTimer('ng.outerAsync(${_source(workFn)})') != false); $root.$eval(workFn); } catch (e, s) { _exceptionHandler(e, s); } finally { assert(_perf.stopTimer('ng.outerAsync(${_source(workFn)})') != false); } } } finally { _clearPhase(); } }
dynamic $emit(name, [List args]) #
$emit(name, [List args]) { var empty = [], namedListeners, scope = this, event = new ScopeEvent(name, this), listenerArgs = [event], i; if (args != null) { listenerArgs.addAll(args); } do { namedListeners = scope._listeners[name]; if (namedListeners != null) { event.currentScope = scope; i = 0; for (var length = namedListeners.length; i<length; i++) { try { relaxFnApply(namedListeners[i], listenerArgs); if (event.propagationStopped) return event; } catch (e, s) { _exceptionHandler(e, s); } } } //traverse upwards scope = scope.$parent; } while (scope != null); return event; }
dynamic $eval(expr, [locals]) #
$eval(expr, [locals]) { return relaxFnArgs(_compileToFn(expr))(this, locals); }
dynamic $evalAsync(expr, {outsideDigest: false}) #
$evalAsync(expr, {outsideDigest: false}) { if (outsideDigest) { _outerAsyncQueue.add(expr); } else { _innerAsyncQueue.add(expr); } }
dynamic $new([bool isolate = false]) #
$new([bool isolate = false]) { return new Scope._child(this, isolate, _perf); }
dynamic $on(name, listener) #
$on(name, listener) { var namedListeners = _listeners[name]; if (!_listeners.containsKey(name)) { _listeners[name] = namedListeners = []; } namedListeners.add(listener); return () { namedListeners.remove(listener); }; }
dynamic $watch(watchExp, [Function listener, String watchStr]) #
$watch(watchExp, [Function listener, String watchStr]) { if (watchStr == null) { watchStr = watchExp.toString(); // Keep prod fast assert((() { watchStr = _source(watchExpr); return true; })()); } var watcher = new _Watch(_compileToFn(listener), _initWatchVal, _compileToFn(watchExp), watchStr); // we use unshift since we use a while loop in $digest for speed. // the while loop reads in reverse order. _watchers.insert(0, watcher); return () => _watchers.remove(watcher); }
dynamic $watchCollection(obj, listener, [String expression]) #
$watchCollection(obj, listener, [String expression]) { var oldValue; var newValue; num changeDetected = 0; Function objGetter = _compileToFn(obj); List internalArray = []; Map internalMap = {}; num oldLength = 0; var $watchCollectionWatch = (_) { newValue = objGetter(this); var newLength, key; if (newValue is! Map && newValue is! List) { if (!_identical(oldValue, newValue)) { oldValue = newValue; changeDetected++; } } else if (newValue is List) { if (!_identical(oldValue, internalArray)) { // we are transitioning from something which was not an array into array. oldValue = internalArray; oldLength = oldValue.length = 0; changeDetected++; } newLength = newValue.length; if (oldLength != newLength) { // if lengths do not match we need to trigger change notification changeDetected++; oldValue.length = oldLength = newLength; } // copy the items to oldValue and look for changes. for (var i = 0; i < newLength; i++) { if (!_identical(oldValue[i], newValue[i])) { changeDetected++; oldValue[i] = newValue[i]; } } } else { // Map if (!_identical(oldValue, internalMap)) { // we are transitioning from something which was not an object into object. oldValue = internalMap = {}; oldLength = 0; changeDetected++; } // copy the items to oldValue and look for changes. newLength = 0; newValue.forEach((key, value) { newLength++; if (oldValue.containsKey(key)) { if (!_identical(oldValue[key], value)) { changeDetected++; oldValue[key] = value; } } else { oldLength++; oldValue[key] = value; changeDetected++; } }); if (oldLength > newLength) { // we used to have more keys, need to find them and destroy them. changeDetected++; var keysToRemove = []; oldValue.forEach((key, _) { if (!newValue.containsKey(key)) { oldLength--; keysToRemove.add(key); } }); keysToRemove.forEach((k) { oldValue.remove(k); }); } } return changeDetected; }; var $watchCollectionAction = (_, __, ___) { relaxFnApply(listener, [newValue, oldValue, this]); }; return this.$watch($watchCollectionWatch, $watchCollectionAction, expression == null ? obj : expression); }
abstract void addAll(Map<K, V> other) #
Adds all key-value pairs of other to this map.
If a key of other is already in this map, its value is overwritten.
The operation is equivalent to doing this[key] = value
for each key
and associated value in other. It iterates over
other, which must
therefore not change during the iteration.
dynamic containsKey(String name) #
Returns true if this map contains the given key.
containsKey(String name) => this[name] != null;
abstract bool containsValue(Object value) #
Returns true if this map contains the given value.
abstract void forEach(void f(K key, V value)) #
Applies f to each {key, value} pair of the map.
It is an error to add or remove keys from the map during iteration.
dynamic noSuchMethod(Invocation invocation) #
noSuchMethod is invoked when users invoke a non-existant method on an object. The name of the method and the arguments of the invocation are passed to noSuchMethod in an Invocation. If noSuchMethod returns a value, that value becomes the result of the original invocation.
The default behavior of noSuchMethod is to throw a
noSuchMethodError
.
noSuchMethod(Invocation invocation) { var name = MirrorSystem.getName(invocation.memberName); if (invocation.isGetter) { return this[name]; } else if (invocation.isSetter) { var value = invocation.positionalArguments[0]; name = name.substring(0, name.length - 1); this[name] = value; return value; } else { if (this[name] is Function) { return this[name](); } else { super.noSuchMethod(invocation); } } }
abstract V putIfAbsent(K key, V ifAbsent()) #
If key is not associated to a value, calls ifAbsent and updates the map by mapping key to the value returned by ifAbsent. Returns the value in the map.
Map<String, int> scores = {'Bob': 36};
for (var key in ['Bob', 'Rohan', 'Sophena']) {
scores.putIfAbsent(key, () => key.length);
}
scores['Bob']; // 36
scores['Rohan']; // 5
scores['Sophena']; // 7
The code that ifAbsent executes must not add or remove keys.
dynamic remove(String name) #
Removes the association for the given key
. Returns the value for
key
in the map or null if key
is not in the map. Note that values
can be null and a returned null value does not always imply that the
key is absent.
remove(String name) => this._properties.remove(name);