Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(602)

Side by Side Diff: pkg/observe/lib/src/path_observer.dart

Issue 132403010: big update to observe, template_binding, polymer (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 6 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 library observe.src.path_observer; 5 library observe.src.path_observer;
6 6
7 import 'dart:async'; 7 import 'dart:async';
8 import 'dart:collection';
8 @MirrorsUsed(metaTargets: const [Reflectable, ObservableProperty], 9 @MirrorsUsed(metaTargets: const [Reflectable, ObservableProperty],
9 override: 'observe.src.path_observer') 10 override: 'observe.src.path_observer')
10 import 'dart:mirrors'; 11 import 'dart:mirrors';
11 import 'package:logging/logging.dart' show Logger, Level; 12 import 'package:logging/logging.dart' show Logger, Level;
12 import 'package:observe/observe.dart'; 13 import 'package:observe/observe.dart';
13 import 'package:observe/src/observable.dart' show objectType; 14 import 'package:observe/src/observable.dart' show objectType;
14 15
15 // This code is inspired by ChangeSummary: 16 /// A data-bound path starting from a view-model or model object, for example
16 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js 17 /// `foo.bar.baz`.
17 // ...which underlies MDV. Since we don't need the functionality of 18 ///
18 // ChangeSummary, we just implement what we need for data bindings. 19 /// When the [observed] stream is being listened to, this will observe changes
Siggi Cherem (dart-lang) 2014/02/03 22:52:48 update doc? seems like there are no streams anymor
Jennifer Messerly 2014/02/04 00:33:06 good catch! done
19 // This allows our implementation to be much simpler. 20 /// to the object and any intermediate object along the path, and send
20 21 /// [observed] accordingly. When all listeners are unregistered it will stop
21 /** 22 /// observing the objects.
22 * A data-bound path starting from a view-model or model object, for example 23 ///
23 * `foo.bar.baz`. 24 /// This class is used to implement `Node.bind` and similar functionality in
24 * 25 /// the [template_binding](pub.dartlang.org/packages/template_binding) package.
25 * When the [values] stream is being listened to, this will observe changes to 26 class PathObserver extends _Observer implements Bindable {
26 * the object and any intermediate object along the path, and send [values] 27 PropertyPath _path;
27 * accordingly. When all listeners are unregistered it will stop observing 28 Object _object;
28 * the objects. 29 _ObservedSet _directObserver;
29 * 30
30 * This class is used to implement [Node.bind] and similar functionality. 31 /// Observes [path] on [object] for changes. This returns an object
31 */ 32 /// that can be used to get the changes and get/set the value at this path.
33 ///
34 /// The path can be a [PropertyPath], or a [String] used to construct it.
35 ///
36 /// See [PathObserver.open] and [PathObserver.value].
Siggi Cherem (dart-lang) 2014/02/03 22:52:48 should this be just [open] and [value] ?
Jennifer Messerly 2014/02/04 00:33:06 Done.
37 PathObserver(Object object, [path])
38 : _object = object,
39 _path = path is PropertyPath ? path : new PropertyPath(path);
40
41 bool get _isClosed => _path == null;
42
43 /// Sets the value at this path.
44 @reflectable void set value(Object newValue) {
45 if (_path != null) _path.setValueFrom(_object, newValue);
46 }
47
48 void _connect() {
49 if (_notifyExpectedArgs > 2) {
Siggi Cherem (dart-lang) 2014/02/03 22:52:48 consider overriding [open] here so we can add more
Jennifer Messerly 2014/02/04 00:33:06 Done.
50 throw new ArgumentError('callback should take 2 or fewer arguments');
51 }
52 _directObserver = new _ObservedSet(this, _object);
53 _check(skipChanges: true);
54 }
55
56 void _disconnect() {
57 _value = null;
58 if (_directObserver != null) {
59 _directObserver.close(this);
60 _directObserver = null;
61 }
62 // Dart note: the JS impl does not do this, but it seems consistent with
63 // CompoundObserver. After closing the PathObserver can't be reopened.
64 _path = null;
65 _object = null;
66 }
67
68 void _iterateObjects(void observe(obj)) {
69 _path._iterateObjects(_object, observe);
70 }
71
72 bool _check({bool skipChanges: false}) {
73 var oldValue = _value;
74 _value = _path.getValueFrom(_object);
75 if (skipChanges || _value == oldValue) return false;
76
77 _report(_value, oldValue);
78 return true;
79 }
80 }
81
82 /// A dot-delimieted property path such as "foo.bar" or "foo.10.bar".
83 /// The path specifies how to get a particular value from an object graph, where
84 /// the graph can include arrays.
32 // TODO(jmesserly): consider specialized subclasses for: 85 // TODO(jmesserly): consider specialized subclasses for:
33 // * empty path 86 // * empty path
34 // * "value" 87 // * "value"
35 // * single token in path, e.g. "foo" 88 // * single token in path, e.g. "foo"
36 class PathObserver extends ChangeNotifier { 89 class PropertyPath {
37 /** The path string. */ 90 /// The segments of the path.
38 final String path;
39
40 /** True if the path is valid, otherwise false. */
41 final bool _isValid;
42
43 final List<Object> _segments; 91 final List<Object> _segments;
44 List<Object> _values; 92
45 List<StreamSubscription> _subs; 93 /// Creates a new [PropertyPath]. These can be stored to avoid excessive
46 94 /// parsing of path strings.
47 final Function _computeValue; 95 ///
48 96 /// The provided [path] should be a String or a List. If it is a list it
49 /** 97 /// should contain only Symbols and integers. This can be used to avoid
50 * Observes [path] on [object] for changes. This returns an object that can be 98 /// parsing.
51 * used to get the changes and get/set the value at this path. 99 ///
52 * 100 /// Note that this constructor will canonicalize identical paths in some cases
53 * You can optionally use [computeValue] to apply a function to the result of 101 /// to save memory, but this is not guaranteed. Use [==] for comparions
54 * evaluating the path. The function should be pure, as PathObserver will not 102 /// purposes instead of [identical].
55 * know to observe any of its dependencies. If you need to observe mutliple 103 factory PropertyPath([path]) {
56 * values, use [CompoundPathObserver] instead. 104 if (path is List) {
57 * 105 var copy = new List.from(path, growable: false);
58 * See [PathObserver.bindSync] and [PathObserver.value]. 106 for (var segment in copy) {
59 */ 107 if (segment is! int && segment is! Symbol) {
60 PathObserver(Object object, String path, {computeValue(newValue)}) 108 throw new ArgumentError('List must contain only ints and Symbols');
61 : path = path, 109 }
62 _computeValue = computeValue,
63 _isValid = _isPathValid(path),
64 _segments = <Object>[] {
65
66 if (_isValid) {
67 for (var segment in path.trim().split('.')) {
68 if (segment == '') continue;
69 var index = int.parse(segment, radix: 10, onError: (_) => null);
70 _segments.add(index != null ? index : new Symbol(segment));
71 } 110 }
72 } 111 return new PropertyPath._(copy);
73 112 }
74 // Initialize arrays. 113
75 // Note that the path itself can't change after it is initially 114 if (path == null) path = '';
76 // constructed, even though the objects along the path can change. 115
77 _values = new List<Object>(_segments.length + 1); 116 var pathObj = _pathCache[path];
78 117 if (pathObj != null) return pathObj;
79 // If we have an empty path, we need to apply the transformation function 118
80 // to the value. The "value" property should always show the transformed 119 if (!_isPathValid(path)) return _InvalidPropertyPath._instance;
81 // value. 120
82 if (_segments.isEmpty && computeValue != null) { 121 final segments = [];
83 object = computeValue(object); 122 for (var segment in path.trim().split('.')) {
84 } 123 if (segment == '') continue;
85 124 var index = int.parse(segment, radix: 10, onError: (_) => null);
86 _values[0] = object; 125 segments.add(index != null ? index : new Symbol(segment));
87 _subs = new List<StreamSubscription>(_segments.length); 126 }
88 } 127
89 128 // TODO(jmesserly): we could use an UnmodifiableListView here, but that adds
90 /** The object being observed. If the path is empty this will be [value]. */ 129 // memory overhead.
91 get object => _values[0]; 130 pathObj = new PropertyPath._(segments.toList(growable: false));
92 131 if (_pathCache.length >= _pathCacheLimit) {
93 /** Gets the last reported value at this path. */ 132 _pathCache.remove(_pathCache.keys.first);
94 @reflectable get value { 133 }
95 if (!_isValid) return null; 134 _pathCache[path] = pathObj;
96 if (!hasObservers) _updateValues(); 135 return pathObj;
97 return _values.last; 136 }
98 } 137
99 138 PropertyPath._(this._segments);
100 /** Sets the value at this path. */ 139
101 @reflectable void set value(Object newValue) { 140 int get length => _segments.length;
141 bool get isEmpty => _segments.isEmpty;
142 bool get isValid => true;
143
144 String toString() {
145 if (!isValid) return '<invalid path>';
146 return _segments
147 .map((s) => s is Symbol ? MirrorSystem.getName(s) : s)
148 .join('.');
149 }
150
151 bool operator ==(other) {
152 if (identical(this, other)) return true;
Siggi Cherem (dart-lang) 2014/02/03 22:52:48 I'm surprised that this is not on by default and t
Jennifer Messerly 2014/02/04 00:33:06 yeah. Here's an example :) class Foo { operator =
153 if (other is! PropertyPath) return false;
154 if (isValid != other.isValid) return false;
155
102 int len = _segments.length; 156 int len = _segments.length;
103 157 if (len != other._segments.length) return false;
104 // TODO(jmesserly): throw if property cannot be set? 158 for (int i = 0; i < len; i++) {
105 // MDV seems tolerant of these errors. 159 if (_segments[i] != other._segments[i]) return false;
106 if (len == 0) return; 160 }
107 if (!hasObservers) _updateValues(end: len - 1); 161 return true;
108 162 }
109 if (_setObjectProperty(_values[len - 1], _segments[len - 1], newValue)) { 163
110 // Technically, this would get updated asynchronously via a change record. 164 /// This is the [Jenkins hash function][1] but using masking to keep
111 // However, it is nice if calling the getter will yield the same value 165 /// values in SMI range.
112 // that was just set. So we use this opportunity to update our cache. 166 /// [1]: http://en.wikipedia.org/wiki/Jenkins_hash_function
Siggi Cherem (dart-lang) 2014/02/03 22:52:48 consider adding a TODO here with the bug number we
Jennifer Messerly 2014/02/04 00:33:06 done. https://code.google.com/p/dart/issues/detail
113 _values[len] = newValue; 167 int get hashCode {
114 } 168 int hash = 0;
115 } 169 for (int i = 0, len = _segments.length; i < len; i++) {
116 170 hash = 0x1fffffff & (hash + _segments[i].hashCode);
117 /** 171 hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
118 * Invokes the [callback] immediately with the current [value], and every time 172 hash = hash ^ (hash >> 6);
119 * the value changes. This is useful for bindings, which want to be up-to-date 173 }
120 * immediately and stay bound to the value of the path. 174 hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
121 */ 175 hash = hash ^ (hash >> 11);
122 StreamSubscription bindSync(void callback(value)) { 176 return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
123 var result = changes.listen((records) { callback(value); }); 177 }
124 callback(value); 178
125 return result; 179 /// Returns the current of the path from the provided [object].
126 } 180 getValueFrom(Object object) {
127 181 if (!isValid) return null;
128 void observed() { 182 for (var segment in _segments) {
129 super.observed(); 183 object = _getObjectProperty(object, segment);
130 _updateValues(); 184 }
131 _observePath(); 185 return object;
132 } 186 }
133 187
134 void unobserved() { 188 /// Attempts to set the [value] of the path from the provided [object].
135 for (int i = 0; i < _subs.length; i++) { 189 /// Returns true if and only if the path was reachable and set.
136 if (_subs[i] != null) { 190 bool setValueFrom(Object object, Object value) {
137 _subs[i].cancel(); 191 var end = _segments.length - 1;
138 _subs[i] = null; 192 if (end < 0) return false;
139 }
140 }
141 super.unobserved();
142 }
143
144 // TODO(jmesserly): should we be caching these values if not observing?
145 void _updateValues({int end}) {
146 if (end == null) end = _segments.length;
147 int last = _segments.length - 1;
148 for (int i = 0; i < end; i++) { 193 for (int i = 0; i < end; i++) {
149 var newValue = _getObjectProperty(_values[i], _segments[i]); 194 object = _getObjectProperty(object, _segments[i]);
150 if (i == last && _computeValue != null) { 195 }
151 newValue = _computeValue(newValue); 196 return _setObjectProperty(object, _segments[end], value);
152 } 197 }
153 _values[i + 1] = newValue; 198
154 } 199 void _iterateObjects(Object obj, void observe(obj)) {
Siggi Cherem (dart-lang) 2014/02/03 22:52:48 nit: obj => object? (I'm fine with either, just to
Jennifer Messerly 2014/02/04 00:33:06 I've kinda learned to just keep whatever the names
155 } 200 if (!isValid || isEmpty) return;
156 201
157 void _updateObservedValues({int start: 0}) { 202 int i = 0, last = _segments.length - 1;
158 var oldValue, newValue; 203 while (obj != null) {
Siggi Cherem (dart-lang) 2014/02/03 22:52:48 should we check for null and break the loop also i
Jennifer Messerly 2014/02/04 00:33:06 Sure thing.
159 for (int i = start, last = _segments.length - 1; i <= last; i++) { 204 observe(obj);
160 oldValue = _values[i + 1]; 205
161 newValue = _getObjectProperty(_values[i], _segments[i]); 206 if (i >= last) break;
162 if (i == last && _computeValue != null) { 207 obj = _getObjectProperty(obj, _segments[i++]);
163 newValue = _computeValue(newValue); 208 }
164 } 209 }
165 if (identical(oldValue, newValue)) { 210 }
166 _observePath(start, i); 211
167 return; 212 class _InvalidPropertyPath extends PropertyPath {
168 } 213 static final _instance = new _InvalidPropertyPath();
169 _values[i + 1] = newValue; 214
170 } 215 bool get isValid => false;
171 216 _InvalidPropertyPath() : super._([]);
172 _observePath(start);
173 notifyPropertyChange(#value, oldValue, newValue);
174 }
175
176 void _observePath([int start = 0, int end]) {
177 if (end == null) end = _segments.length;
178
179 for (int i = start; i < end; i++) {
180 if (_subs[i] != null) _subs[i].cancel();
181 _observeIndex(i);
182 }
183 }
184
185 void _observeIndex(int i) {
186 final object = _values[i];
187 final segment = _segments[i];
188 if (segment is int) {
189 if (object is ObservableList) {
190 _subs[i] = object.listChanges.listen((List<ListChangeRecord> records) {
191 for (var record in records) {
192 if (record.indexChanged(segment)) {
193 _updateObservedValues(start: i);
194 return;
195 }
196 }
197 });
198 }
199 } else if (object is Observable) {
200 // TODO(jmesserly): rather than allocating a new closure for each
201 // property, we could try and have one for the entire path. However we'd
202 // need to do a linear scan to find the index as soon as we got a change.
203 // Also we need to fix ListChangeRecord and MapChangeRecord to contain
204 // the target. Not sure if it's worth it.
205
206 _subs[i] = object.changes.listen((List<ChangeRecord> records) {
207 for (var record in records) {
208 if (_changeRecordMatches(record, segment)) {
209 _updateObservedValues(start: i);
210 return;
211 }
212 }
213 });
214 }
215 }
216 } 217 }
217 218
218 bool _changeRecordMatches(record, key) { 219 bool _changeRecordMatches(record, key) {
219 if (record is PropertyChangeRecord) { 220 if (record is PropertyChangeRecord) {
220 return (record as PropertyChangeRecord).name == key; 221 return (record as PropertyChangeRecord).name == key;
221 } 222 }
222 if (record is MapChangeRecord) { 223 if (record is MapChangeRecord) {
223 if (key is Symbol) key = MirrorSystem.getName(key); 224 if (key is Symbol) key = MirrorSystem.getName(key);
224 return (record as MapChangeRecord).key == key; 225 return (record as MapChangeRecord).key == key;
225 } 226 }
(...skipping 85 matching lines...) Expand 10 before | Expand all | Expand 10 after
311 while (type != objectType) { 312 while (type != objectType) {
312 final members = type.declarations; 313 final members = type.declarations;
313 if (members[name] is VariableMirror) return true; 314 if (members[name] is VariableMirror) return true;
314 if (members.containsKey(setterName)) return true; 315 if (members.containsKey(setterName)) return true;
315 if (members.containsKey(#noSuchMethod)) return true; 316 if (members.containsKey(#noSuchMethod)) return true;
316 type = _safeSuperclass(type); 317 type = _safeSuperclass(type);
317 } 318 }
318 return false; 319 return false;
319 } 320 }
320 321
321 /** 322 /// True if the type has a method, other than on Object.
322 * True if the type has a method, other than on Object. 323 /// Doesn't consider noSuchMethod, unless [name] is `#noSuchMethod`.
323 * Doesn't consider noSuchMethod, unless [name] is `#noSuchMethod`.
324 */
325 bool _hasMethod(ClassMirror type, Symbol name) { 324 bool _hasMethod(ClassMirror type, Symbol name) {
326 while (type != objectType) { 325 while (type != objectType) {
327 final member = type.declarations[name]; 326 final member = type.declarations[name];
328 if (member is MethodMirror && member.isRegularMethod) return true; 327 if (member is MethodMirror && member.isRegularMethod) return true;
329 type = _safeSuperclass(type); 328 type = _safeSuperclass(type);
330 } 329 }
331 return false; 330 return false;
332 } 331 }
333 332
334 ClassMirror _safeSuperclass(ClassMirror type) { 333 ClassMirror _safeSuperclass(ClassMirror type) {
(...skipping 14 matching lines...) Expand all
349 final _pathRegExp = () { 348 final _pathRegExp = () {
350 const identStart = '[\$_a-zA-Z]'; 349 const identStart = '[\$_a-zA-Z]';
351 const identPart = '[\$_a-zA-Z0-9]'; 350 const identPart = '[\$_a-zA-Z0-9]';
352 const ident = '$identStart+$identPart*'; 351 const ident = '$identStart+$identPart*';
353 const elementIndex = '(?:[0-9]|[1-9]+[0-9]+)'; 352 const elementIndex = '(?:[0-9]|[1-9]+[0-9]+)';
354 const identOrElementIndex = '(?:$ident|$elementIndex)'; 353 const identOrElementIndex = '(?:$ident|$elementIndex)';
355 const path = '(?:$identOrElementIndex)(?:\\.$identOrElementIndex)*'; 354 const path = '(?:$identOrElementIndex)(?:\\.$identOrElementIndex)*';
356 return new RegExp('^$path\$'); 355 return new RegExp('^$path\$');
357 }(); 356 }();
358 357
359 final _spacesRegExp = new RegExp(r'\s');
360
361 bool _isPathValid(String s) { 358 bool _isPathValid(String s) {
362 s = s.replaceAll(_spacesRegExp, ''); 359 s = s.trim();
363
364 if (s == '') return true; 360 if (s == '') return true;
365 if (s[0] == '.') return false; 361 if (s[0] == '.') return false;
366 return _pathRegExp.hasMatch(s); 362 return _pathRegExp.hasMatch(s);
367 } 363 }
368 364
369 final Logger _logger = new Logger('observe.PathObserver'); 365 final Logger _logger = new Logger('observe.PathObserver');
366
367
368 /// This is a simple cache. It's like LRU but we don't update an item on a
369 /// cache hit, because that would require allocation. Better to let it expire
370 /// and reallocate the PropertyPath.
371 // TODO(jmesserly): this optimization is from observe-js, how valuable is it in
372 // practice?
373 final _pathCache = new LinkedHashMap<String, PropertyPath>();
374
375 /// The size of a path like "foo.bar" is approximately 160 bytes, so this
376 /// reserves ~16Kb of memory for recently used paths. Since paths are frequently
377 /// reused, the theory is that this ends up being a good tradeoff in practice.
378 // (Note: the 160 byte estimate is from Dart VM 1.0.0.10_r30798 on x64 without
379 // using UnmodifiableListView in PropertyPath)
Siggi Cherem (dart-lang) 2014/02/03 22:52:48 seems like you switched to use ungrowable list? -
Jennifer Messerly 2014/02/04 00:33:06 yeah, ungrowable is what I tested. It's Unmodifiab
380 const int _pathCacheLimit = 100;
381
382 /// CompoundObserver is a [Bindable] object which knows how to listen to
Siggi Cherem (dart-lang) 2014/02/03 22:52:48 +[] around CompoundObserver
Jennifer Messerly 2014/02/04 00:33:06 Done. I've heard the opposite comment before thoug
383 /// multiple values (registered via [addPath] or [addObserver]) and invoke a
384 /// callback when one or more of the values have changed.
385 ///
386 /// var obj = new ObservableMap.from({'a': 1, 'b': 2});
387 /// var otherObj = new ObservableMap.from({'c': 3});
388 ///
389 /// var observer = new CompoundObserver()
390 /// ..addPath(obj, 'a');
391 /// ..addObserver(new PathObserver(obj, 'b'));
392 /// ..addPath(otherObj, 'c');
393 /// ..open((values) {
394 /// for (int i = 0; i < values.length; i++) {
395 /// print('The value at index $i is now ${values[i]}');
396 /// }
397 /// });
398 ///
399 /// obj['a'] = 10; // print will be triggered async
400 ///
401 class CompoundObserver extends _Observer implements Bindable {
402 _ObservedSet _directObserver;
403 List _observed = [];
404
405 bool get _isClosed => _observed == null;
406
407 CompoundObserver() {
408 _value = [];
409 }
410
411 void _connect() {
412 if (_notifyExpectedArgs > 3) {
413 throw new ArgumentError('callback should take 3 or fewer arguments');
414 }
415 _check(skipChanges: true);
416
417 for (var i = 0; i < _observed.length; i += 2) {
418 var object = _observed[i];
419 if (!identical(object, _observerSentinel)) {
420 _directObserver = new _ObservedSet(this, object);
421 break;
422 }
423 }
424 }
425
426 void _disconnect() {
427 _value = null;
428
429 if (_directObserver != null) {
430 _directObserver.close(this);
431 _directObserver = null;
432 }
433
434 for (var i = 0; i < _observed.length; i += 2) {
435 if (identical(_observed[i], _observerSentinel)) {
436 _observed[i + 1].close();
437 }
438 }
439 _observed = null;
440 }
441
442 /// Adds a dependency on the property [path] accessed from [object].
443 /// [path] can be a [PropertyPath] or a [String]. If it is omitted an empty
444 /// path will be used.
445 void addPath(Object object, [path]) {
446 if (_isOpen || _isClosed) {
447 throw new StateError('Cannot add paths once started.');
448 }
449
450 if (path is! PropertyPath) path = new PropertyPath(path);
451 _observed..add(object)..add(path);
452 }
453
454 void addObserver(Bindable observer) {
455 if (_isOpen || _isClosed) {
456 throw new StateError('Cannot add observers once started.');
457 }
458
459 observer.open(_deliver);
460 _observed..add(_observerSentinel)..add(observer);
461 }
462
463 void _iterateObjects(void observe(obj)) {
464 for (var i = 0; i < _observed.length; i += 2) {
465 var object = _observed[i];
466 if (!identical(object, _observerSentinel)) {
467 (_observed[i + 1] as PropertyPath)._iterateObjects(object, observe);
468 }
469 }
470 }
471
472 bool _check({bool skipChanges: false}) {
473 bool changed = false;
474 _value.length = _observed.length ~/ 2;
475 var oldValues = null;
476 for (var i = 0; i < _observed.length; i += 2) {
477 var pathOrObserver = _observed[i + 1];
478 var object = _observed[i];
479 var value = identical(object, _observerSentinel) ?
480 (pathOrObserver as Bindable).value :
481 (pathOrObserver as PropertyPath).getValueFrom(object);
482
483 if (skipChanges) {
484 _value[i ~/ 2] = value;
485 continue;
486 }
487
488 if (value == _value[i ~/ 2]) {
489 continue;
490 }
Siggi Cherem (dart-lang) 2014/02/03 22:52:48 nit: single line if (value == _...) continue;
Jennifer Messerly 2014/02/04 00:33:06 Done.
491
492 // don't allocate this unless necessary.
493 if (_notifyExpectedArgs >= 2) {
494 if (oldValues == null) oldValues = new Map();
495 oldValues[i ~/ 2] = _value[i ~/ 2];
496 }
497
498 changed = true;
499 _value[i ~/ 2] = value;
500 }
501
502 if (!changed) return false;
503
504 // TODO(rafaelw): Having _observed as the third callback arg here is
505 // pretty lame API. Fix.
506 _report(_value, oldValues, _observed);
507 return true;
508 }
509 }
510
511 const _observerSentinel = const _ObserverSentinel();
512 class _ObserverSentinel { const _ObserverSentinel(); }
513
514 // A base class for the shared API implemented by PathObserver and
515 // CompoundObserver and used in _ObservedSet.
516 abstract class _Observer extends Bindable {
517 static int _nextBirthId = 0;
518
519 /// A number indicating when the object was created.
520 final int _birthId = _nextBirthId++;
521
522 Function _notifyCallback;
523 int _notifyExpectedArgs;
524 var _value;
525
526 // abstract members
527 void _iterateObjects(void observe(obj));
528 void _connect();
529 void _disconnect();
530 bool get _isClosed;
531 _check({bool skipChanges: false});
532
533 bool get _isOpen => _notifyCallback != null;
534
535 open(callback) {
536 if (_isOpen || _isClosed) {
537 throw new StateError('Observer has already been opened.');
538 }
539
540 _notifyCallback = callback;
541 _notifyExpectedArgs = _argumentCount(callback);
542 _connect();
543 return _value;
544 }
545
546 @reflectable get value {
547 _check(skipChanges: true);
548 return _value;
549 }
550
551 void close() {
552 if (!_isOpen) return;
553
554 _disconnect();
555 _value = null;
556 _notifyCallback = null;
557 }
558
559 void _deliver(_) {
560 if (_isOpen) _dirtyCheck();
561 }
562
563 bool _dirtyCheck() {
564 var cycles = 0;
565 while (cycles < _MAX_DIRTY_CHECK_CYCLES && _check()) {
566 cycles++;
567 }
568 return cycles > 0;
569 }
570
571 void _report(newValue, oldValue, [extraArg]) {
572 try {
573 switch (_notifyExpectedArgs) {
574 case 0: _notifyCallback(); break;
575 case 1: _notifyCallback(newValue); break;
576 case 2: _notifyCallback(newValue, oldValue); break;
577 case 3: _notifyCallback(newValue, oldValue, extraArg); break;
578 }
579 } catch (e, s) {
580 // Deliver errors async, so if a single callback fails it doesn't prevent
581 // other things from working.
582 new Completer().completeError(e, s);
583 }
584 }
585 }
586
587 typedef _Func0();
588 typedef _Func1(a);
589 typedef _Func2(a, b);
590 typedef _Func3(a, b, c);
591
592 int _argumentCount(fn) {
Siggi Cherem (dart-lang) 2014/02/03 22:52:48 woah, I'm surprised this works! we should use this
Jennifer Messerly 2014/02/04 00:33:06 good catch. Fixed
593 if (fn is _Func0) return 0;
594 if (fn is _Func1) return 1;
595 if (fn is _Func2) return 2;
596 if (fn is _Func3) return 3;
597 return 4; // treat as invalid.
Siggi Cherem (dart-lang) 2014/02/03 22:52:48 maybe -1?
Jennifer Messerly 2014/02/04 00:33:06 funny, tried that first, but using something like
598 }
599
600
601 class _ObservedSet {
602 /// To prevent sequential [PathObserver]s and [CompoundObserver]s from
603 /// observing the same object, we check if they are observing the same root
604 /// as the most recently created observer, and if so merge it into the
605 /// existing _ObservedSet.
606 ///
607 /// See <https://github.com/Polymer/observe-js/commit/f0990b1> and
608 /// <https://codereview.appspot.com/46780044/>.
609 static _ObservedSet _lastSet;
610
611 /// The root object for a [PathObserver]. For a [CompoundObserver], the root
612 /// object of the first path observed. This is used by the constructor to
613 /// reuse an [_ObservedSet] that starts from the same object.
614 Object _rootObject;
615
616 /// Observers associated with this root object, in birth order.
617 final Map<int, _Observer> _observers = new SplayTreeMap();
618
619 // Dart note: the JS implementation is O(N^2) because Array.indexOf is used
620 // for lookup in these two arrays. We use HashMap to avoid this problem. It
621 // also gives us a nice way of tracking the StreamSubscription.
622 Map<Object, StreamSubscription> _objects;
623 Map<Object, StreamSubscription> _toRemove;
624
625 bool _resetNeeded = false;
626
627 factory _ObservedSet(_Observer observer, Object rootObj) {
628 if (_lastSet == null || !identical(_lastSet._rootObject, rootObj)) {
629 _lastSet = new _ObservedSet._(rootObj);
630 }
631 _lastSet.open(observer);
632 }
633
634 _ObservedSet._(this._rootObject);
635
636 void open(_Observer obs) {
637 _observers[obs._birthId] = obs;
638 obs._iterateObjects(observe);
639 }
640
641 void close(_Observer obs) {
642 var anyLeft = false;
643
644 _observers.remove(obs._birthId);
645
646 if (_observers.isNotEmpty) {
647 _resetNeeded = true;
648 scheduleMicrotask(reset);
649 return;
650 }
651 _resetNeeded = false;
652
653 if (_objects != null) {
654 for (var sub in _objects) sub.cancel();
655 _objects = null;
656 }
657 }
658
659 void observe(Object obj) {
660 if (obj is ObservableList) _observeStream(obj.listChanges);
661 if (obj is Observable) _observeStream(obj.changes);
662 }
663
664 void _observeStream(Stream stream) {
665 // TODO(jmesserly): we hash on streams as we have two separate change
666 // streams for ObservableList. Not sure if that is the design we will use
667 // going forward.
668
669 if (_objects == null) _objects = new HashMap();
670 StreamSubscription sub = null;
671 if (_toRemove != null) sub = _toRemove.remove(stream);
672 if (sub != null) {
673 _objects[stream] = sub;
674 } else if (!_objects.containsKey(stream)) {
675 _objects[stream] = stream.listen(_callback);
676 }
677 }
678
679 void reset() {
680 if (!_resetNeeded) return;
681
682 var objs = _toRemove == null ? new HashMap() : _toRemove;
683 _toRemove = _objects;
684 _objects = objs;
685 for (var observer in _observers.values) {
686 if (observer._isOpen) observer._iterateObjects(observe);
687 }
688
689 for (var sub in _toRemove.values) sub.cancel();
690
691 _toRemove = null;
692 }
693
694 void _callback(records) {
695 for (var observer in _observers.values.toList(growable: false)) {
696 if (observer._isOpen) observer._check();
697 }
698
699 _resetNeeded = true;
700 scheduleMicrotask(reset);
701 }
702 }
703
704 const int _MAX_DIRTY_CHECK_CYCLES = 1000;
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698