OLD | NEW |
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 import 'dart:collection'; |
9 import 'dart:math' show min; | 9 import 'dart:math' show min; |
10 | 10 |
(...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
75 var oldValue = _value; | 75 var oldValue = _value; |
76 _value = _path.getValueFrom(_object); | 76 _value = _path.getValueFrom(_object); |
77 if (skipChanges || _value == oldValue) return false; | 77 if (skipChanges || _value == oldValue) return false; |
78 | 78 |
79 _report(_value, oldValue); | 79 _report(_value, oldValue); |
80 return true; | 80 return true; |
81 } | 81 } |
82 } | 82 } |
83 | 83 |
84 /// A dot-delimieted property path such as "foo.bar" or "foo.10.bar". | 84 /// A dot-delimieted property path such as "foo.bar" or "foo.10.bar". |
85 /// | |
86 /// The path specifies how to get a particular value from an object graph, where | 85 /// The path specifies how to get a particular value from an object graph, where |
87 /// the graph can include arrays and maps. Each segment of the path describes | 86 /// the graph can include arrays. |
88 /// how to take a single step in the object graph. Properties like 'foo' or | |
89 /// 'bar' are read as properties on objects, or as keys if the object is a [Map] | |
90 /// or a [Indexable], while integer values are read as indexes in a [List]. | |
91 // TODO(jmesserly): consider specialized subclasses for: | 87 // TODO(jmesserly): consider specialized subclasses for: |
92 // * empty path | 88 // * empty path |
93 // * "value" | 89 // * "value" |
94 // * single token in path, e.g. "foo" | 90 // * single token in path, e.g. "foo" |
95 class PropertyPath { | 91 class PropertyPath { |
96 /// The segments of the path. | 92 /// The segments of the path. |
97 final List<Object> _segments; | 93 final List<Object> _segments; |
98 | 94 |
99 /// Creates a new [PropertyPath]. These can be stored to avoid excessive | 95 /// Creates a new [PropertyPath]. These can be stored to avoid excessive |
100 /// parsing of path strings. | 96 /// parsing of path strings. |
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
177 for (int i = 0, len = _segments.length; i < len; i++) { | 173 for (int i = 0, len = _segments.length; i < len; i++) { |
178 hash = 0x1fffffff & (hash + _segments[i].hashCode); | 174 hash = 0x1fffffff & (hash + _segments[i].hashCode); |
179 hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); | 175 hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); |
180 hash = hash ^ (hash >> 6); | 176 hash = hash ^ (hash >> 6); |
181 } | 177 } |
182 hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); | 178 hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); |
183 hash = hash ^ (hash >> 11); | 179 hash = hash ^ (hash >> 11); |
184 return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); | 180 return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); |
185 } | 181 } |
186 | 182 |
187 /// Returns the current value of the path from the provided [obj]ect. | 183 /// Returns the current of the path from the provided [obj]ect. |
188 getValueFrom(Object obj) { | 184 getValueFrom(Object obj) { |
189 if (!isValid) return null; | 185 if (!isValid) return null; |
190 for (var segment in _segments) { | 186 for (var segment in _segments) { |
191 if (obj == null) return null; | 187 if (obj == null) return null; |
192 obj = _getObjectProperty(obj, segment); | 188 obj = _getObjectProperty(obj, segment); |
193 } | 189 } |
194 return obj; | 190 return obj; |
195 } | 191 } |
196 | 192 |
197 /// Attempts to set the [value] of the path from the provided [obj]ect. | 193 /// Attempts to set the [value] of the path from the provided [obj]ect. |
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
230 if (record is PropertyChangeRecord) { | 226 if (record is PropertyChangeRecord) { |
231 return (record as PropertyChangeRecord).name == key; | 227 return (record as PropertyChangeRecord).name == key; |
232 } | 228 } |
233 if (record is MapChangeRecord) { | 229 if (record is MapChangeRecord) { |
234 if (key is Symbol) key = smoke.symbolToName(key); | 230 if (key is Symbol) key = smoke.symbolToName(key); |
235 return (record as MapChangeRecord).key == key; | 231 return (record as MapChangeRecord).key == key; |
236 } | 232 } |
237 return false; | 233 return false; |
238 } | 234 } |
239 | 235 |
240 /// Properties in [Map] that need to be read as properties and not as keys in | |
241 /// the map. We exclude methods ('containsValue', 'containsKey', 'putIfAbsent', | |
242 /// 'addAll', 'remove', 'clear', 'forEach') because there is no use in reading | |
243 /// them as part of path-observer segments. | |
244 const _MAP_PROPERTIES = const [#keys, #values, #length, #isEmpty, #isNotEmpty]; | |
245 | |
246 _getObjectProperty(object, property) { | 236 _getObjectProperty(object, property) { |
247 if (object == null) return null; | 237 if (object == null) return null; |
248 | 238 |
249 if (property is int) { | 239 if (property is int) { |
250 if (object is List && property >= 0 && property < object.length) { | 240 if (object is List && property >= 0 && property < object.length) { |
251 return object[property]; | 241 return object[property]; |
252 } | 242 } |
253 } else if (property is Symbol) { | 243 } else if (property is Symbol) { |
254 // Support indexer if available, e.g. Maps or polymer_expressions Scope. | 244 final type = object.runtimeType; |
255 // This is the default syntax used by polymer/nodebind and | |
256 // polymer/observe-js PathObserver. | |
257 // TODO(sigmund): should we also support using checking dynamically for | |
258 // whether the type practically implements the indexer API | |
259 // (smoke.hasInstanceMethod(type, const Symbol('[]')))? | |
260 if (object is Indexable<String, dynamic> || | |
261 object is Map<String, dynamic> && !_MAP_PROPERTIES.contains(property)) { | |
262 return object[smoke.symbolToName(property)]; | |
263 } | |
264 try { | 245 try { |
265 return smoke.read(object, property); | 246 if (smoke.hasGetter(type, property) || smoke.hasNoSuchMethod(type)) { |
| 247 return smoke.read(object, property); |
| 248 } |
| 249 // Support indexer if available, e.g. Maps or polymer_expressions Scope. |
| 250 // This is the default syntax used by polymer/nodebind and |
| 251 // polymer/observe-js PathObserver. |
| 252 if (smoke.hasInstanceMethod(type, const Symbol('[]'))) { |
| 253 return object[smoke.symbolToName(property)]; |
| 254 } |
266 } on NoSuchMethodError catch (e) { | 255 } on NoSuchMethodError catch (e) { |
267 // Rethrow, unless the type implements noSuchMethod, in which case we | 256 // Rethrow, unless the type implements noSuchMethod, in which case we |
268 // interpret the exception as a signal that the method was not found. | 257 // interpret the exception as a signal that the method was not found. |
269 // Dart note: getting invalid properties is an error, unlike in JS where | 258 if (!smoke.hasNoSuchMethod(type)) rethrow; |
270 // it returns undefined. | |
271 if (!smoke.hasNoSuchMethod(object.runtimeType)) rethrow; | |
272 } | 259 } |
273 } | 260 } |
274 | 261 |
275 if (_logger.isLoggable(Level.FINER)) { | 262 if (_logger.isLoggable(Level.FINER)) { |
276 _logger.finer("can't get $property in $object"); | 263 _logger.finer("can't get $property in $object"); |
277 } | 264 } |
278 return null; | 265 return null; |
279 } | 266 } |
280 | 267 |
281 bool _setObjectProperty(object, property, value) { | 268 bool _setObjectProperty(object, property, value) { |
282 if (object == null) return false; | 269 if (object == null) return false; |
283 | 270 |
284 if (property is int) { | 271 if (property is int) { |
285 if (object is List && property >= 0 && property < object.length) { | 272 if (object is List && property >= 0 && property < object.length) { |
286 object[property] = value; | 273 object[property] = value; |
287 return true; | 274 return true; |
288 } | 275 } |
289 } else if (property is Symbol) { | 276 } else if (property is Symbol) { |
290 // Support indexer if available, e.g. Maps or polymer_expressions Scope. | 277 final type = object.runtimeType; |
291 if (object is Indexable<String, dynamic> || | |
292 object is Map<String, dynamic> && !_MAP_PROPERTIES.contains(property)) { | |
293 object[smoke.symbolToName(property)] = value; | |
294 return true; | |
295 } | |
296 try { | 278 try { |
297 smoke.write(object, property, value); | 279 if (smoke.hasSetter(type, property) || smoke.hasNoSuchMethod(type)) { |
298 return true; | 280 smoke.write(object, property, value); |
299 } on NoSuchMethodError catch (e, s) { | 281 return true; |
300 if (!smoke.hasNoSuchMethod(object.runtimeType)) rethrow; | 282 } |
| 283 // Support indexer if available, e.g. Maps or polymer_expressions Scope. |
| 284 if (smoke.hasInstanceMethod(type, const Symbol('[]='))) { |
| 285 object[smoke.symbolToName(property)] = value; |
| 286 return true; |
| 287 } |
| 288 } on NoSuchMethodError catch (e) { |
| 289 if (!smoke.hasNoSuchMethod(type)) rethrow; |
301 } | 290 } |
302 } | 291 } |
303 | 292 |
304 if (_logger.isLoggable(Level.FINER)) { | 293 if (_logger.isLoggable(Level.FINER)) { |
305 _logger.finer("can't set $property in $object"); | 294 _logger.finer("can't set $property in $object"); |
306 } | 295 } |
307 return false; | 296 return false; |
308 } | 297 } |
309 | 298 |
310 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js | 299 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js |
(...skipping 163 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
474 | 463 |
475 if (!changed) return false; | 464 if (!changed) return false; |
476 | 465 |
477 // TODO(rafaelw): Having _observed as the third callback arg here is | 466 // TODO(rafaelw): Having _observed as the third callback arg here is |
478 // pretty lame API. Fix. | 467 // pretty lame API. Fix. |
479 _report(_value, oldValues, _observed); | 468 _report(_value, oldValues, _observed); |
480 return true; | 469 return true; |
481 } | 470 } |
482 } | 471 } |
483 | 472 |
484 /// An object accepted by [PropertyPath] where properties are read and written | |
485 /// as indexing operations, just like a [Map]. | |
486 abstract class Indexable<K, V> { | |
487 V operator [](K key); | |
488 operator []=(K key, V value); | |
489 } | |
490 | |
491 const _observerSentinel = const _ObserverSentinel(); | 473 const _observerSentinel = const _ObserverSentinel(); |
492 class _ObserverSentinel { const _ObserverSentinel(); } | 474 class _ObserverSentinel { const _ObserverSentinel(); } |
493 | 475 |
494 // A base class for the shared API implemented by PathObserver and | 476 // A base class for the shared API implemented by PathObserver and |
495 // CompoundObserver and used in _ObservedSet. | 477 // CompoundObserver and used in _ObservedSet. |
496 abstract class _Observer extends Bindable { | 478 abstract class _Observer extends Bindable { |
497 static int _nextBirthId = 0; | 479 static int _nextBirthId = 0; |
498 | 480 |
499 /// A number indicating when the object was created. | 481 /// A number indicating when the object was created. |
500 final int _birthId = _nextBirthId++; | 482 final int _birthId = _nextBirthId++; |
(...skipping 169 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
670 for (var observer in _observers.values.toList(growable: false)) { | 652 for (var observer in _observers.values.toList(growable: false)) { |
671 if (observer._isOpen) observer._check(); | 653 if (observer._isOpen) observer._check(); |
672 } | 654 } |
673 | 655 |
674 _resetNeeded = true; | 656 _resetNeeded = true; |
675 scheduleMicrotask(reset); | 657 scheduleMicrotask(reset); |
676 } | 658 } |
677 } | 659 } |
678 | 660 |
679 const int _MAX_DIRTY_CHECK_CYCLES = 1000; | 661 const int _MAX_DIRTY_CHECK_CYCLES = 1000; |
OLD | NEW |