| OLD | NEW |
| (Empty) | |
| 1 part of angular.core; |
| 2 |
| 3 |
| 4 /** |
| 5 * Used by [Scope.$on] to notify the listeners of events. |
| 6 */ |
| 7 class ScopeEvent { |
| 8 String name; |
| 9 Scope targetScope; |
| 10 Scope currentScope; |
| 11 bool propagationStopped = false; |
| 12 bool defaultPrevented = false; |
| 13 |
| 14 ScopeEvent(this.name, this.targetScope); |
| 15 |
| 16 stopPropagation () => propagationStopped = true; |
| 17 preventDefault() => defaultPrevented = true; |
| 18 } |
| 19 |
| 20 /** |
| 21 * Allows the configuration of [Scope.$digest] iteration maximum time-to-live |
| 22 * value. Digest keeps checking the state of the watcher getters until it |
| 23 * can execute one full iteration with no watchers triggering. TTL is used |
| 24 * to prevent an infinite loop where watch A triggers watch B which in turn |
| 25 * triggers watch A. If the system does not stabilize in TTL iteration then |
| 26 * an digest is stop an an exception is thrown. |
| 27 */ |
| 28 @NgInjectableService() |
| 29 class ScopeDigestTTL { |
| 30 final num ttl; |
| 31 ScopeDigestTTL(): ttl = 5; |
| 32 ScopeDigestTTL.value(num this.ttl); |
| 33 } |
| 34 |
| 35 /** |
| 36 * Scope has two responsibilities. 1) to keep track af watches and 2) |
| 37 * to keep references to the model so that they are available for |
| 38 * data-binding. |
| 39 */ |
| 40 @proxy |
| 41 @NgInjectableService() |
| 42 class Scope implements Map { |
| 43 final ExceptionHandler _exceptionHandler; |
| 44 final Parser _parser; |
| 45 final NgZone _zone; |
| 46 final num _ttl; |
| 47 final Map<String, Object> _properties = {}; |
| 48 final _WatchList _watchers = new _WatchList(); |
| 49 final Map<String, List<Function>> _listeners = {}; |
| 50 final bool _isolate; |
| 51 final bool _lazy; |
| 52 final Profiler _perf; |
| 53 final Scope $parent; |
| 54 |
| 55 String $id; |
| 56 Scope $root; |
| 57 num _nextId = 0; |
| 58 String _phase; |
| 59 List _innerAsyncQueue; |
| 60 List _outerAsyncQueue; |
| 61 Scope _nextSibling, _prevSibling, _childHead, _childTail; |
| 62 bool _skipAutoDigest = false; |
| 63 bool _disabled = false; |
| 64 |
| 65 Scope(this._exceptionHandler, this._parser, ScopeDigestTTL ttl, |
| 66 this._zone, this._perf): |
| 67 $parent = null, _isolate = false, _lazy = false, _ttl = ttl.ttl { |
| 68 _properties[r'this']= this; |
| 69 $root = this; |
| 70 $id = '_${$root._nextId++}'; |
| 71 _innerAsyncQueue = []; |
| 72 _outerAsyncQueue = []; |
| 73 |
| 74 // Set up the zone to auto digest this scope. |
| 75 _zone.onTurnDone = _autoDigestOnTurnDone; |
| 76 _zone.onError = (e, s, ls) => _exceptionHandler(e, s); |
| 77 } |
| 78 |
| 79 Scope._child(Scope parent, bool this._isolate, bool this._lazy, Profiler this.
_perf): |
| 80 $parent = parent, _ttl = parent._ttl, _parser = parent._parser, |
| 81 _exceptionHandler = parent._exceptionHandler, _zone = parent._zone { |
| 82 _properties[r'this'] = this; |
| 83 $root = $parent.$root; |
| 84 $id = '_${$root._nextId++}'; |
| 85 _innerAsyncQueue = $parent._innerAsyncQueue; |
| 86 _outerAsyncQueue = $parent._outerAsyncQueue; |
| 87 |
| 88 _prevSibling = $parent._childTail; |
| 89 if ($parent._childHead != null) { |
| 90 $parent._childTail._nextSibling = this; |
| 91 $parent._childTail = this; |
| 92 } else { |
| 93 $parent._childHead = $parent._childTail = this; |
| 94 } |
| 95 } |
| 96 |
| 97 _autoDigestOnTurnDone() { |
| 98 if (_skipAutoDigest) { |
| 99 _skipAutoDigest = false; |
| 100 } else { |
| 101 $digest(); |
| 102 } |
| 103 } |
| 104 |
| 105 _identical(a, b) => |
| 106 identical(a, b) || |
| 107 (a is String && b is String && a == b) || |
| 108 (a is num && b is num && a.isNaN && b.isNaN); |
| 109 |
| 110 containsKey(String name) => this[name] != null; |
| 111 remove(String name) => this._properties.remove(name); |
| 112 operator []=(String name, value) => _properties[name] = value; |
| 113 operator [](String name) { |
| 114 if (name == r'$id') return this.$id; |
| 115 if (name == r'$parent') return this.$parent; |
| 116 if (name == r'$root') return this.$root; |
| 117 var scope = this; |
| 118 do { |
| 119 if (scope._properties.containsKey(name)) { |
| 120 return scope._properties[name]; |
| 121 } else if (!scope._isolate) { |
| 122 scope = scope.$parent; |
| 123 } else { |
| 124 return null; |
| 125 } |
| 126 } while(scope != null); |
| 127 return null; |
| 128 } |
| 129 |
| 130 noSuchMethod(Invocation invocation) { |
| 131 var name = MirrorSystem.getName(invocation.memberName); |
| 132 if (invocation.isGetter) { |
| 133 return this[name]; |
| 134 } else if (invocation.isSetter) { |
| 135 var value = invocation.positionalArguments[0]; |
| 136 name = name.substring(0, name.length - 1); |
| 137 this[name] = value; |
| 138 return value; |
| 139 } else { |
| 140 if (this[name] is Function) { |
| 141 return this[name](); |
| 142 } else { |
| 143 super.noSuchMethod(invocation); |
| 144 } |
| 145 } |
| 146 } |
| 147 |
| 148 |
| 149 /** |
| 150 * Create a new child [Scope]. |
| 151 * |
| 152 * * [isolate] - If set to true the child scope does not inherit properties fr
om the parent scope. |
| 153 * This in essence creates an independent (isolated) view for the users of t
he scope. |
| 154 * * [lazy] - If set to true the scope digest will only run if the scope is ma
rked as [$dirty]. |
| 155 * This is usefull if we expect that the bindings in the scope are constant
and there is no need |
| 156 * to check them on each digest. The digest can be forced by marking it [$di
rty]. |
| 157 */ |
| 158 $new({bool isolate: false, bool lazy: false}) => |
| 159 new Scope._child(this, isolate, lazy, _perf); |
| 160 |
| 161 /** |
| 162 * *EXPERIMENTAL:* This feature is experimental. We reserve the right to chang
e or delete it. |
| 163 * |
| 164 * A dissabled scope will not be part of the [$digest] cycle until it is re-en
abled. |
| 165 */ |
| 166 set $disabled(value) => this._disabled = value; |
| 167 get $disabled => this._disabled; |
| 168 |
| 169 /** |
| 170 * Registers a listener callback to be executed whenever the [watchExpression]
changes. |
| 171 * |
| 172 * The watchExpression is called on every call to [$digest] and should return
the value that |
| 173 * will be watched. (Since [$digest] reruns when it detects changes the watchE
xpression can |
| 174 * execute multiple times per [$digest] and should be idempotent.) |
| 175 * |
| 176 * The listener is called only when the value from the current [watchExpressio
n] and the |
| 177 * previous call to [watchExpression] are not identical (with the exception of
the initial run, |
| 178 * see below). |
| 179 * |
| 180 * The watch listener may change the model, which may trigger other listeners
to fire. This is |
| 181 * achieved by rerunning the watchers until no changes are detected. The rerun
iteration limit |
| 182 * is 10 to prevent an infinite loop deadlock. |
| 183 * If you want to be notified whenever [$digest] is called, you can register a
[watchExpression] |
| 184 * function with no listener. (Since [watchExpression] can execute multiple ti
mes per [$digest] |
| 185 * cycle when a change is detected, be prepared for multiple calls to your lis
tener.) |
| 186 * |
| 187 * After a watcher is registered with the scope, the listener fn is called asy
nchronously |
| 188 * (via [$evalAsync]) to initialize the watcher. In rare cases, this is undesi
rable because the |
| 189 * listener is called when the result of [watchExpression] didn't change. To d
etect this |
| 190 * scenario within the listener fn, you can compare the newVal and oldVal. If
these two values |
| 191 * are identical then the listener was called due to initialization. |
| 192 * |
| 193 * * [watchExpression] - can be any one of these: a [Function] - `(Scope scope
) => ...;` or a |
| 194 * [String] - `expression` which is compiled with [Parser] service into a f
unction |
| 195 * * [listener] - A [Function] `(currentValue, previousValue, Scope scope) =>
...;` |
| 196 * * [watchStr] - Used as a debbuging hint to easier identify which expression
is associated with |
| 197 * this watcher. |
| 198 */ |
| 199 $watch(watchExpression, [Function listener, String watchStr]) { |
| 200 if (watchStr == null) { |
| 201 watchStr = watchExpression.toString(); |
| 202 |
| 203 // Keep prod fast |
| 204 assert((() { |
| 205 watchStr = _source(watchExpression); |
| 206 return true; |
| 207 })()); |
| 208 } |
| 209 var watcher = new _Watch(_compileToFn(listener), _initWatchVal, |
| 210 _compileToFn(watchExpression), watchStr); |
| 211 _watchers.addLast(watcher); |
| 212 return () => _watchers.remove(watcher); |
| 213 } |
| 214 |
| 215 /** |
| 216 * A variant of [$watch] where it watches a collection of [watchExpressios]. I
f any |
| 217 * one expression in the collection changes the [listener] is executed. |
| 218 * |
| 219 * * [watcherExpressions] - `List<String|(Scope scope){}>` |
| 220 * * [Listener] - `(List newValues, List previousValues, Scope scope)` |
| 221 */ |
| 222 $watchSet(List watchExpressions, [Function listener, String watchStr]) { |
| 223 if (watchExpressions.length == 0) return () => null; |
| 224 |
| 225 var lastValues = new List(watchExpressions.length); |
| 226 var currentValues = new List(watchExpressions.length); |
| 227 |
| 228 if (watchExpressions.length == 1) { |
| 229 // Special case size of one. |
| 230 return $watch(watchExpressions[0], (value, oldValue, scope) { |
| 231 currentValues[0] = value; |
| 232 lastValues[0] = oldValue; |
| 233 listener(currentValues, lastValues, scope); |
| 234 }); |
| 235 } |
| 236 var deregesterFns = []; |
| 237 var changeCount = 0; |
| 238 for(var i = 0, ii = watchExpressions.length; i < ii; i++) { |
| 239 deregesterFns.add($watch(watchExpressions[i], (value, oldValue, __) { |
| 240 currentValues[i] = value; |
| 241 lastValues[i] = oldValue; |
| 242 changeCount++; |
| 243 })); |
| 244 } |
| 245 deregesterFns.add($watch((s) => changeCount, (c, o, scope) { |
| 246 listener(currentValues, lastValues, scope); |
| 247 })); |
| 248 return () { |
| 249 for(var i = 0, ii = deregesterFns.length; i < ii; i++) { |
| 250 deregesterFns[i](); |
| 251 } |
| 252 }; |
| 253 } |
| 254 |
| 255 /** |
| 256 * Shallow watches the properties of an object and fires whenever any of the p
roperties change |
| 257 * (for arrays, this implies watching the array items; for object maps, this i
mplies watching |
| 258 * the properties). If a change is detected, the listener callback is fired. |
| 259 * |
| 260 * The obj collection is observed via standard [$watch] operation and is exam
ined on every call |
| 261 * to [$digest] to see if any items have been added, removed, or moved. |
| 262 * |
| 263 * The listener is called whenever anything within the obj has changed. Examp
les include |
| 264 * adding, removing, and moving items belonging to an object or array. |
| 265 */ |
| 266 $watchCollection(obj, listener, [String expression, bool shallow=false]) { |
| 267 var oldValue; |
| 268 var newValue; |
| 269 int changeDetected = 0; |
| 270 Function objGetter = _compileToFn(obj); |
| 271 List internalArray = []; |
| 272 Map internalMap = {}; |
| 273 int oldLength = 0; |
| 274 int newLength; |
| 275 var key; |
| 276 List keysToRemove = []; |
| 277 Function detectNewKeys = (key, value) { |
| 278 newLength++; |
| 279 if (oldValue.containsKey(key)) { |
| 280 if (!_identical(oldValue[key], value)) { |
| 281 changeDetected++; |
| 282 oldValue[key] = value; |
| 283 } |
| 284 } else { |
| 285 oldLength++; |
| 286 oldValue[key] = value; |
| 287 changeDetected++; |
| 288 } |
| 289 }; |
| 290 Function findMissingKeys = (key, _) { |
| 291 if (!newValue.containsKey(key)) { |
| 292 oldLength--; |
| 293 keysToRemove.add(key); |
| 294 } |
| 295 }; |
| 296 |
| 297 Function removeMissingKeys = (k) => oldValue.remove(k); |
| 298 |
| 299 var $watchCollectionWatch; |
| 300 |
| 301 if (shallow) { |
| 302 $watchCollectionWatch = (_) { |
| 303 newValue = objGetter(this); |
| 304 newLength = newValue == null ? 0 : newValue.length; |
| 305 if (newLength != oldLength) { |
| 306 oldLength = newLength; |
| 307 changeDetected++; |
| 308 } |
| 309 if (!identical(oldValue, newValue)) { |
| 310 oldValue = newValue; |
| 311 changeDetected++; |
| 312 } |
| 313 return changeDetected; |
| 314 }; |
| 315 } else { |
| 316 $watchCollectionWatch = (_) { |
| 317 newValue = objGetter(this); |
| 318 |
| 319 if (newValue is! Map && newValue is! List) { |
| 320 if (!_identical(oldValue, newValue)) { |
| 321 oldValue = newValue; |
| 322 changeDetected++; |
| 323 } |
| 324 } else if (newValue is Iterable) { |
| 325 if (!_identical(oldValue, internalArray)) { |
| 326 // we are transitioning from something which was not an array into a
rray. |
| 327 oldValue = internalArray; |
| 328 oldLength = oldValue.length = 0; |
| 329 changeDetected++; |
| 330 } |
| 331 |
| 332 newLength = newValue.length; |
| 333 |
| 334 if (oldLength != newLength) { |
| 335 // if lengths do not match we need to trigger change notification |
| 336 changeDetected++; |
| 337 oldValue.length = oldLength = newLength; |
| 338 } |
| 339 // copy the items to oldValue and look for changes. |
| 340 for (var i = 0; i < newLength; i++) { |
| 341 if (!_identical(oldValue[i], newValue.elementAt(i))) { |
| 342 changeDetected++; |
| 343 oldValue[i] = newValue.elementAt(i); |
| 344 } |
| 345 } |
| 346 } else { // Map |
| 347 if (!_identical(oldValue, internalMap)) { |
| 348 // we are transitioning from something which was not an object into
object. |
| 349 oldValue = internalMap = {}; |
| 350 oldLength = 0; |
| 351 changeDetected++; |
| 352 } |
| 353 // copy the items to oldValue and look for changes. |
| 354 newLength = 0; |
| 355 newValue.forEach(detectNewKeys); |
| 356 if (oldLength > newLength) { |
| 357 // we used to have more keys, need to find them and destroy them. |
| 358 changeDetected++; |
| 359 oldValue.forEach(findMissingKeys); |
| 360 keysToRemove.forEach(removeMissingKeys); |
| 361 keysToRemove.clear(); |
| 362 } |
| 363 } |
| 364 return changeDetected; |
| 365 }; |
| 366 } |
| 367 |
| 368 var $watchCollectionAction = (_, __, ___) { |
| 369 relaxFnApply(listener, [newValue, oldValue, this]); |
| 370 }; |
| 371 |
| 372 return this.$watch($watchCollectionWatch, |
| 373 $watchCollectionAction, |
| 374 expression == null ? obj : expression); |
| 375 } |
| 376 |
| 377 |
| 378 /** |
| 379 * Add this function to your code if you want to add a $digest |
| 380 * and want to assert that the digest will be called on this turn. |
| 381 * This method will be deleted when we are comfortable with |
| 382 * auto-digesting scope. |
| 383 */ |
| 384 $$verifyDigestWillRun() { |
| 385 assert(!_skipAutoDigest); |
| 386 _zone.assertInTurn(); |
| 387 } |
| 388 |
| 389 /** |
| 390 * *EXPERIMENTAL:* This feature is experimental. We reserve the right to chang
e or delete it. |
| 391 * |
| 392 * Marks a scope as dirty. If the scope is lazy (see [$new]) then the scope wi
ll be included |
| 393 * in the next [$digest]. |
| 394 * |
| 395 * NOTE: This has no effect for non-lazy scopes. |
| 396 */ |
| 397 $dirty() { |
| 398 this._disabled = false; |
| 399 } |
| 400 |
| 401 $digest() { |
| 402 try { |
| 403 _beginPhase('\$digest'); |
| 404 _digestWhileDirtyLoop(); |
| 405 } catch (e, s) { |
| 406 _exceptionHandler(e, s); |
| 407 } finally { |
| 408 _clearPhase(); |
| 409 } |
| 410 } |
| 411 |
| 412 |
| 413 _digestWhileDirtyLoop() { |
| 414 _digestHandleQueue('ng.innerAsync', _innerAsyncQueue); |
| 415 |
| 416 int timerId; |
| 417 assert((timerId = _perf.startTimer('ng.dirty_check', 0)) != false); |
| 418 _Watch lastDirtyWatch = _digestComputeLastDirty(); |
| 419 assert(_perf.stopTimer(timerId) != false); |
| 420 |
| 421 if (lastDirtyWatch == null) { |
| 422 _digestHandleQueue('ng.outerAsync', _outerAsyncQueue); |
| 423 return; |
| 424 } |
| 425 |
| 426 List<List<String>> watchLog = []; |
| 427 for (int iteration = 1, ttl = _ttl; iteration < ttl; iteration++) { |
| 428 _Watch stopWatch = _digestHandleQueue('ng.innerAsync', _innerAsyncQueue) |
| 429 ? null // Evaluating async work requires re-evaluating all watchers. |
| 430 : lastDirtyWatch; |
| 431 lastDirtyWatch = null; |
| 432 |
| 433 List<String> expressionLog; |
| 434 if (ttl - iteration <= 3) { |
| 435 expressionLog = <String>[]; |
| 436 watchLog.add(expressionLog); |
| 437 } |
| 438 |
| 439 int timerId; |
| 440 assert((timerId = _perf.startTimer('ng.dirty_check', iteration)) != false)
; |
| 441 lastDirtyWatch = _digestComputeLastDirtyUntil(stopWatch, expressionLog); |
| 442 assert(_perf.stopTimer(timerId) != false); |
| 443 |
| 444 if (lastDirtyWatch == null) { |
| 445 _digestComputePerfCounters(); |
| 446 _digestHandleQueue('ng.outerAsync', _outerAsyncQueue); |
| 447 return; |
| 448 } |
| 449 } |
| 450 |
| 451 // I've seen things you people wouldn't believe. Attack ships on fire |
| 452 // off the shoulder of Orion. I've watched C-beams glitter in the dark |
| 453 // near the Tannhauser Gate. All those moments will be lost in time, |
| 454 // like tears in rain. Time to die. |
| 455 throw '$_ttl \$digest() iterations reached. Aborting!\n' |
| 456 'Watchers fired in the last ${watchLog.length} iterations: ' |
| 457 '${_toJson(watchLog)}'; |
| 458 } |
| 459 |
| 460 |
| 461 bool _digestHandleQueue(String timerName, List queue) { |
| 462 if (queue.isEmpty) { |
| 463 return false; |
| 464 } |
| 465 do { |
| 466 var timerId; |
| 467 try { |
| 468 var workFn = queue.removeAt(0); |
| 469 assert((timerId = _perf.startTimer(timerName, _source(workFn))) != false
); |
| 470 $root.$eval(workFn); |
| 471 } catch (e, s) { |
| 472 _exceptionHandler(e, s); |
| 473 } finally { |
| 474 assert(_perf.stopTimer(timerId) != false); |
| 475 } |
| 476 } while (queue.isNotEmpty); |
| 477 return true; |
| 478 } |
| 479 |
| 480 |
| 481 _Watch _digestComputeLastDirty() { |
| 482 int watcherCount = 0; |
| 483 int scopeCount = 0; |
| 484 Scope scope = this; |
| 485 do { |
| 486 _WatchList watchers = scope._watchers; |
| 487 watcherCount += watchers.length; |
| 488 scopeCount++; |
| 489 for (_Watch watch = watchers.head; watch != null; watch = watch.next) { |
| 490 var last = watch.last; |
| 491 var value = watch.get(scope); |
| 492 if (!_identical(value, last)) { |
| 493 return _digestHandleDirty(scope, watch, last, value, null); |
| 494 } |
| 495 } |
| 496 } while ((scope = _digestComputeNextScope(scope)) != null); |
| 497 _digestUpdatePerfCounters(watcherCount, scopeCount); |
| 498 return null; |
| 499 } |
| 500 |
| 501 |
| 502 _Watch _digestComputeLastDirtyUntil(_Watch stopWatch, List<String> log) { |
| 503 int watcherCount = 0; |
| 504 int scopeCount = 0; |
| 505 Scope scope = this; |
| 506 do { |
| 507 _WatchList watchers = scope._watchers; |
| 508 watcherCount += watchers.length; |
| 509 scopeCount++; |
| 510 for (_Watch watch = watchers.head; watch != null; watch = watch.next) { |
| 511 if (identical(stopWatch, watch)) return null; |
| 512 var last = watch.last; |
| 513 var value = watch.get(scope); |
| 514 if (!_identical(value, last)) { |
| 515 return _digestHandleDirty(scope, watch, last, value, log); |
| 516 } |
| 517 } |
| 518 } while ((scope = _digestComputeNextScope(scope)) != null); |
| 519 return null; |
| 520 } |
| 521 |
| 522 |
| 523 _Watch _digestHandleDirty(Scope scope, _Watch watch, last, value, List<String>
log) { |
| 524 _Watch lastDirtyWatch; |
| 525 while (true) { |
| 526 if (!_identical(value, last)) { |
| 527 lastDirtyWatch = watch; |
| 528 if (log != null) log.add(watch.exp == null ? '[unknown]' : watch.exp); |
| 529 watch.last = value; |
| 530 var fireTimer; |
| 531 assert((fireTimer = _perf.startTimer('ng.fire', watch.exp)) != false); |
| 532 watch.fn(value, identical(_initWatchVal, last) ? value : last, scope); |
| 533 assert(_perf.stopTimer(fireTimer) != false); |
| 534 } |
| 535 watch = watch.next; |
| 536 while (watch == null) { |
| 537 scope = _digestComputeNextScope(scope); |
| 538 if (scope == null) return lastDirtyWatch; |
| 539 watch = scope._watchers.head; |
| 540 } |
| 541 last = watch.last; |
| 542 value = watch.get(scope); |
| 543 } |
| 544 } |
| 545 |
| 546 |
| 547 Scope _digestComputeNextScope(Scope scope) { |
| 548 // Insanity Warning: scope depth-first traversal |
| 549 // yes, this code is a bit crazy, but it works and we have tests to prove it
! |
| 550 // this piece should be kept in sync with the traversal in $broadcast |
| 551 Scope target = this; |
| 552 Scope childHead = scope._childHead; |
| 553 while (childHead != null && childHead._disabled) { |
| 554 childHead = childHead._nextSibling; |
| 555 } |
| 556 if (childHead == null) { |
| 557 if (scope == target) { |
| 558 return null; |
| 559 } else { |
| 560 Scope next = scope._nextSibling; |
| 561 if (next == null) { |
| 562 while (scope != target && (next = scope._nextSibling) == null) { |
| 563 scope = scope.$parent; |
| 564 } |
| 565 } |
| 566 return next; |
| 567 } |
| 568 } else { |
| 569 if (childHead._lazy) childHead._disabled = true; |
| 570 return childHead; |
| 571 } |
| 572 } |
| 573 |
| 574 |
| 575 void _digestComputePerfCounters() { |
| 576 int watcherCount = 0, scopeCount = 0; |
| 577 Scope scope = this; |
| 578 do { |
| 579 scopeCount++; |
| 580 watcherCount += scope._watchers.length; |
| 581 } while ((scope = _digestComputeNextScope(scope)) != null); |
| 582 _digestUpdatePerfCounters(watcherCount, scopeCount); |
| 583 } |
| 584 |
| 585 |
| 586 void _digestUpdatePerfCounters(int watcherCount, int scopeCount) { |
| 587 _perf.counters['ng.scope.watchers'] = watcherCount; |
| 588 _perf.counters['ng.scopes'] = scopeCount; |
| 589 } |
| 590 |
| 591 |
| 592 $destroy() { |
| 593 if ($root == this) return; // we can't remove the root node; |
| 594 |
| 595 $broadcast(r'$destroy'); |
| 596 |
| 597 if ($parent._childHead == this) $parent._childHead = _nextSibling; |
| 598 if ($parent._childTail == this) $parent._childTail = _prevSibling; |
| 599 if (_prevSibling != null) _prevSibling._nextSibling = _nextSibling; |
| 600 if (_nextSibling != null) _nextSibling._prevSibling = _prevSibling; |
| 601 } |
| 602 |
| 603 |
| 604 $eval(expr, [locals]) { |
| 605 return relaxFnArgs(_compileToFn(expr))(locals == null ? this : new ScopeLoca
ls(this, locals)); |
| 606 } |
| 607 |
| 608 |
| 609 $evalAsync(expr, {outsideDigest: false}) { |
| 610 if (outsideDigest) { |
| 611 _outerAsyncQueue.add(expr); |
| 612 } else { |
| 613 _innerAsyncQueue.add(expr); |
| 614 } |
| 615 } |
| 616 |
| 617 |
| 618 /** |
| 619 * Skip running a $digest at the end of this turn. |
| 620 * The primary use case is to skip the digest in the current VM turn because |
| 621 * you just scheduled or are otherwise certain of an impending VM turn and the |
| 622 * digest at the end of that turn is sufficient. You should be able to answer |
| 623 * "No" to the question "Is there any other code that is aware that this VM |
| 624 * turn occured and therefore expected a digest?". If your answer is "Yes", |
| 625 * then you run the risk that the very next VM turn is not for your event and |
| 626 * now that other code runs in that turn and sees stale values. |
| 627 * |
| 628 * You might call this function, for instance, from an event listener where, |
| 629 * though the event occured, you need to wait for another event before you can |
| 630 * perform something meaningful. You might schedule that other event, |
| 631 * set a flag for the handler of the other event to recognize, etc. and then |
| 632 * call this method to skip the digest this cycle. Note that you should call |
| 633 * this function *after* you have successfully confirmed that the expected VM |
| 634 * turn will occur (perhaps by scheduling it) to ensure that the digest |
| 635 * actually does take place on that turn. |
| 636 */ |
| 637 $skipAutoDigest() { |
| 638 _zone.assertInTurn(); |
| 639 _skipAutoDigest = true; |
| 640 } |
| 641 |
| 642 |
| 643 $apply([expr]) { |
| 644 return _zone.run(() { |
| 645 var timerId; |
| 646 try { |
| 647 assert((timerId = _perf.startTimer('ng.\$apply', _source(expr))) != fals
e); |
| 648 return $eval(expr); |
| 649 } catch (e, s) { |
| 650 _exceptionHandler(e, s); |
| 651 } finally { |
| 652 assert(_perf.stopTimer(timerId) != false); |
| 653 } |
| 654 }); |
| 655 } |
| 656 |
| 657 |
| 658 $on(name, listener) { |
| 659 var namedListeners = _listeners[name]; |
| 660 if (!_listeners.containsKey(name)) { |
| 661 _listeners[name] = namedListeners = []; |
| 662 } |
| 663 namedListeners.add(listener); |
| 664 |
| 665 return () { |
| 666 namedListeners.remove(listener); |
| 667 }; |
| 668 } |
| 669 |
| 670 |
| 671 $emit(name, [List args]) { |
| 672 var empty = [], |
| 673 namedListeners, |
| 674 scope = this, |
| 675 event = new ScopeEvent(name, this), |
| 676 listenerArgs = [event], |
| 677 i; |
| 678 |
| 679 if (args != null) { |
| 680 listenerArgs.addAll(args); |
| 681 } |
| 682 |
| 683 do { |
| 684 namedListeners = scope._listeners[name]; |
| 685 if (namedListeners != null) { |
| 686 event.currentScope = scope; |
| 687 i = 0; |
| 688 for (var length = namedListeners.length; i<length; i++) { |
| 689 try { |
| 690 relaxFnApply(namedListeners[i], listenerArgs); |
| 691 if (event.propagationStopped) return event; |
| 692 } catch (e, s) { |
| 693 _exceptionHandler(e, s); |
| 694 } |
| 695 } |
| 696 } |
| 697 //traverse upwards |
| 698 scope = scope.$parent; |
| 699 } while (scope != null); |
| 700 |
| 701 return event; |
| 702 } |
| 703 |
| 704 |
| 705 $broadcast(String name, [List listenerArgs]) { |
| 706 var target = this, |
| 707 current = target, |
| 708 next = target, |
| 709 event = new ScopeEvent(name, this); |
| 710 |
| 711 //down while you can, then up and next sibling or up and next sibling until
back at root |
| 712 if (listenerArgs == null) { |
| 713 listenerArgs = []; |
| 714 } |
| 715 listenerArgs.insert(0, event); |
| 716 do { |
| 717 current = next; |
| 718 event.currentScope = current; |
| 719 if (current._listeners.containsKey(name)) { |
| 720 current._listeners[name].forEach((listener) { |
| 721 try { |
| 722 relaxFnApply(listener, listenerArgs); |
| 723 } catch(e, s) { |
| 724 _exceptionHandler(e, s); |
| 725 } |
| 726 }); |
| 727 } |
| 728 |
| 729 // Insanity Warning: scope depth-first traversal |
| 730 // yes, this code is a bit crazy, but it works and we have tests to prove
it! |
| 731 // this piece should be kept in sync with the traversal in $broadcast |
| 732 if (current._childHead == null) { |
| 733 if (current == target) { |
| 734 next = null; |
| 735 } else { |
| 736 next = current._nextSibling; |
| 737 if (next == null) { |
| 738 while(current != target && (next = current._nextSibling) == null) { |
| 739 current = current.$parent; |
| 740 } |
| 741 } |
| 742 } |
| 743 } else { |
| 744 next = current._childHead; |
| 745 } |
| 746 } while ((current = next) != null); |
| 747 |
| 748 return event; |
| 749 } |
| 750 |
| 751 _beginPhase(phase) { |
| 752 if ($root._phase != null) { |
| 753 // TODO(deboer): Remove the []s when dartbug.com/11999 is fixed. |
| 754 throw ['${$root._phase} already in progress']; |
| 755 } |
| 756 assert(_perf.startTimer('ng.phase.${phase}') != false); |
| 757 |
| 758 $root._phase = phase; |
| 759 } |
| 760 |
| 761 _clearPhase() { |
| 762 assert(_perf.stopTimer('ng.phase.${$root._phase}') != false); |
| 763 $root._phase = null; |
| 764 } |
| 765 |
| 766 Function _compileToFn(exp) { |
| 767 if (exp == null) { |
| 768 return () => null; |
| 769 } else if (exp is String) { |
| 770 return _parser(exp).eval; |
| 771 } else if (exp is Function) { |
| 772 return exp; |
| 773 } else { |
| 774 throw 'Expecting String or Function'; |
| 775 } |
| 776 } |
| 777 } |
| 778 |
| 779 @proxy |
| 780 class ScopeLocals implements Scope, Map { |
| 781 static wrapper(dynamic scope, Map<String, Object> locals) => new ScopeLocals(s
cope, locals); |
| 782 |
| 783 dynamic _scope; |
| 784 Map<String, Object> _locals; |
| 785 |
| 786 ScopeLocals(this._scope, this._locals); |
| 787 |
| 788 operator []=(String name, value) => _scope[name] = value; |
| 789 operator [](String name) => (_locals.containsKey(name) ? _locals : _scope)[nam
e]; |
| 790 |
| 791 noSuchMethod(Invocation invocation) => mirror.reflect(_scope).delegate(invocat
ion); |
| 792 } |
| 793 |
| 794 class _InitWatchVal { const _InitWatchVal(); } |
| 795 const _initWatchVal = const _InitWatchVal(); |
| 796 |
| 797 class _Watch { |
| 798 final Function fn; |
| 799 final Function get; |
| 800 final String exp; |
| 801 var last; |
| 802 |
| 803 _Watch previous; |
| 804 _Watch next; |
| 805 |
| 806 _Watch(fn, this.last, getFn, this.exp) |
| 807 : this.fn = relaxFnArgs3(fn) |
| 808 , this.get = relaxFnArgs1(getFn); |
| 809 } |
| 810 |
| 811 class _WatchList { |
| 812 int length = 0; |
| 813 _Watch head; |
| 814 _Watch tail; |
| 815 |
| 816 void addLast(_Watch watch) { |
| 817 assert(watch.previous == null); |
| 818 assert(watch.next == null); |
| 819 if (tail == null) { |
| 820 tail = head = watch; |
| 821 } else { |
| 822 watch.previous = tail; |
| 823 tail.next = watch; |
| 824 tail = watch; |
| 825 } |
| 826 length++; |
| 827 } |
| 828 |
| 829 void remove(_Watch watch) { |
| 830 if (watch == head) { |
| 831 _Watch next = watch.next; |
| 832 if (next == null) tail = null; |
| 833 else next.previous = null; |
| 834 head = next; |
| 835 } else if (watch == tail) { |
| 836 _Watch previous = watch.previous; |
| 837 previous.next = null; |
| 838 tail = previous; |
| 839 } else { |
| 840 _Watch next = watch.next; |
| 841 _Watch previous = watch.previous; |
| 842 previous.next = next; |
| 843 next.previous = previous; |
| 844 } |
| 845 length--; |
| 846 } |
| 847 } |
| 848 |
| 849 _toJson(obj) { |
| 850 try { |
| 851 return JSON.encode(obj); |
| 852 } catch(e) { |
| 853 var ret = "NOT-JSONABLE"; |
| 854 // Keep prod fast. |
| 855 assert((() { |
| 856 var mirror = reflect(obj); |
| 857 if (mirror is ClosureMirror) { |
| 858 // work-around dartbug.com/14130 |
| 859 try { |
| 860 ret = mirror.function.source; |
| 861 } on NoSuchMethodError catch (e) {} |
| 862 } |
| 863 return true; |
| 864 })()); |
| 865 return ret; |
| 866 } |
| 867 } |
| 868 |
| 869 String _source(obj) { |
| 870 if (obj is Function) { |
| 871 var m = reflect(obj); |
| 872 if (m is ClosureMirror) { |
| 873 // work-around dartbug.com/14130 |
| 874 try { |
| 875 return "FN: ${m.function.source}"; |
| 876 } on NoSuchMethodError catch (e) { |
| 877 } on UnimplementedError catch (e) { |
| 878 } |
| 879 } |
| 880 } |
| 881 return '$obj'; |
| 882 } |
| OLD | NEW |