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

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

Issue 19771010: implement dirty checking for @observable objects (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: logging for loops in dirty checking Created 7 years, 5 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 part of observe; 5 part of observe;
6 6
7 // This code is inspired by ChangeSummary: 7 // This code is inspired by ChangeSummary:
8 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js 8 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js
9 // ...which underlies MDV. Since we don't need the functionality of 9 // ...which underlies MDV. Since we don't need the functionality of
10 // ChangeSummary, we just implement what we need for data bindings. 10 // ChangeSummary, we just implement what we need for data bindings.
11 // This allows our implementation to be much simpler. 11 // This allows our implementation to be much simpler.
12 12
13 // TODO(jmesserly): should we make these types stronger, and require
14 // Observable objects? Currently, it is fine to say something like:
15 // var path = new PathObserver(123, '');
16 // print(path.value); // "123"
17 //
18 // Furthermore this degenerate case is allowed:
19 // var path = new PathObserver(123, 'foo.bar.baz.qux');
20 // print(path.value); // "null"
21 //
22 // Here we see that any invalid (i.e. not Observable) value will break the
23 // path chain without producing an error or exception.
24 //
25 // Now the real question: should we do this? For the former case, the behavior
26 // is correct but we could chose to handle it in the dart:html bindings layer.
27 // For the latter case, it might be better to throw an error so users can find
28 // the problem.
29
30
31 /** 13 /**
32 * A data-bound path starting from a view-model or model object, for example 14 * A data-bound path starting from a view-model or model object, for example
33 * `foo.bar.baz`. 15 * `foo.bar.baz`.
34 * 16 *
35 * When the [values] stream is being listened to, this will observe changes to 17 * When the [values] stream is being listened to, this will observe changes to
36 * the object and any intermediate object along the path, and send [values] 18 * the object and any intermediate object along the path, and send [values]
37 * accordingly. When all listeners are unregistered it will stop observing 19 * accordingly. When all listeners are unregistered it will stop observing
38 * the objects. 20 * the objects.
39 * 21 *
40 * This class is used to implement [Node.bind] and similar functionality. 22 * This class is used to implement [Node.bind] and similar functionality.
41 */ 23 */
42 // TODO(jmesserly): find a better home for this type. 24 // TODO(jmesserly): find a better home for this type.
43 class PathObserver { 25 class PathObserver extends ChangeNotifierBase {
44 /** The object being observed. */
45 final object;
46
47 /** The path string. */ 26 /** The path string. */
48 final String path; 27 final String path;
49 28
50 /** True if the path is valid, otherwise false. */ 29 /** True if the path is valid, otherwise false. */
51 final bool _isValid; 30 final bool _isValid;
52 31
53 // TODO(jmesserly): same issue here as ObservableMixin: is there an easier 32 final List<Object> _segments;
54 // way to get a broadcast stream? 33 List<Object> _values;
55 StreamController _values; 34 List<StreamSubscription> _subs;
56 Stream _valueStream;
57
58 _PropertyObserver _observer, _lastObserver;
59
60 Object _lastValue;
61 bool _scheduled = false;
62 35
63 /** 36 /**
64 * Observes [path] on [object] for changes. This returns an object that can be 37 * Observes [path] on [object] for changes. This returns an object that can be
65 * used to get the changes and get/set the value at this path. 38 * used to get the changes and get/set the value at this path.
66 * See [PathObserver.values] and [PathObserver.value]. 39 * See [PathObserver.bindSync] and [PathObserver.value].
67 */ 40 */
68 PathObserver(this.object, String path) 41 PathObserver(Object object, String path)
69 : path = path, _isValid = _isPathValid(path) { 42 : path = path,
70 43 _isValid = _isPathValid(path),
71 // TODO(jmesserly): if the path is empty, or the object is! Observable, we 44 _segments = <Object>[] {
72 // can optimize the PathObserver to be more lightweight.
73
74 _values = new StreamController.broadcast(sync: true,
75 onListen: _observe,
76 onCancel: _unobserve);
77 45
78 if (_isValid) { 46 if (_isValid) {
79 var segments = [];
80 for (var segment in path.trim().split('.')) { 47 for (var segment in path.trim().split('.')) {
81 if (segment == '') continue; 48 if (segment == '') continue;
82 var index = int.parse(segment, onError: (_) {}); 49 var index = int.parse(segment, onError: (_) => null);
83 segments.add(index != null ? index : new Symbol(segment)); 50 _segments.add(index != null ? index : new Symbol(segment));
84 } 51 }
52 }
85 53
86 // Create the property observer linked list. 54 // Initialize arrays.
87 // Note that the structure of a path can't change after it is initially 55 // Note that the path itself can't change after it is initially
88 // constructed, even though the objects along the path can change. 56 // constructed, even though the objects along the path can change.
89 for (int i = segments.length - 1; i >= 0; i--) { 57 _values = new List<Object>(_segments.length + 1);
90 _observer = new _PropertyObserver(this, segments[i], _observer); 58 _values[0] = object;
91 if (_lastObserver == null) _lastObserver = _observer; 59 _subs = new List<StreamSubscription>(_segments.length);
60 }
61
62 /** The object being observed. */
63 get object => _values[0];
64
65 /** Gets the last reported value at this path. */
66 get value {
67 if (!_isValid) return null;
68 if (!hasObservers) _updateValues();
69 return _values.last;
70 }
71
72 /** Sets the value at this path. */
73 void set value(Object value) {
74 int len = _segments.length;
75
76 // TODO(jmesserly): throw if property cannot be set?
77 // MDV seems tolerant of these errors.
78 if (len == 0) return;
79 if (!hasObservers) _updateValues();
80
81 if (_setObjectProperty(_values[len - 1], _segments[len - 1], value)) {
82 // Technically, this would get updated asynchronously via a change record.
83 // However, it is nice if calling the getter will yield the same value
84 // that was just set. So we use this opportunity to update our cache.
85 _values[len] = value;
86 }
87 }
88
89 /**
90 * Invokes the [callback] immediately with the current [value], and every time
91 * the value changes. This is useful for bindings, which want to be up-to-date
92 * immediately and stay bound to the value of the path.
93 */
94 StreamSubscription bindSync(void callback(value)) {
95 var result = changes.listen((records) { callback(value); });
96 callback(value);
97 return result;
98 }
99
100 void _observed() {
101 super._observed();
102 _updateValues();
103 _observePath();
104 }
105
106 void _unobserved() {
107 for (int i = 0; i < _subs.length; i++) {
108 if (_subs[i] != null) {
109 _subs[i].cancel();
110 _subs[i] = null;
92 } 111 }
93 } 112 }
94 } 113 }
95 114
96 // TODO(jmesserly): we could try adding the first value to the stream, but 115 // TODO(jmesserly): should we be caching these values if not observing?
97 // that delivers the first record async. 116 void _updateValues() {
98 /** 117 for (int i = 0; i < _segments.length; i++) {
99 * Listens to the stream, and invokes the [callback] immediately with the 118 _values[i + 1] = _getObjectProperty(_values[i], _segments[i]);
100 * current [value]. This is useful for bindings, which want to be up-to-date
101 * immediately.
102 */
103 StreamSubscription bindSync(void callback(value)) {
104 var result = values.listen(callback);
105 callback(value);
106 return result;
107 }
108
109 // TODO(jmesserly): should this be a change record with the old value?
110 // TODO(jmesserly): should this be a broadcast stream? We only need
111 // single-subscription in the bindings system, so single sub saves overhead.
112 /**
113 * Gets the stream of values that were observed at this path.
114 * This returns a single-subscription stream.
115 */
116 Stream get values => _values.stream;
117
118 /** Force synchronous delivery of [values]. */
119 void _deliverValues() {
120 _scheduled = false;
121
122 var newValue = value;
123 if (!identical(_lastValue, newValue)) {
124 _values.add(newValue);
125 _lastValue = newValue;
126 } 119 }
127 } 120 }
128 121
129 void _observe() { 122 void _updateObservedValues([int start = 0]) {
130 if (_observer != null) { 123 bool changed = false;
131 _lastValue = value; 124 for (int i = start; i < _segments.length; i++) {
132 _observer.observe(); 125 final newValue = _getObjectProperty(_values[i], _segments[i]);
126 if (identical(_values[i + 1], newValue)) {
127 _observePath(start, i);
128 return;
129 }
130 _values[i + 1] = newValue;
131 changed = true;
132 }
133
134 _observePath(start);
135 if (changed) {
136 notifyChange(new PropertyChangeRecord(const Symbol('value')));
133 } 137 }
134 } 138 }
135 139
136 void _unobserve() { 140 void _observePath([int start = 0, int end]) {
137 if (_observer != null) _observer.unobserve(); 141 if (end == null) end = _segments.length;
142
143 for (int i = start; i < end; i++) {
144 if (_subs[i] != null) _subs[i].cancel();
145 _observeIndex(i);
146 }
138 } 147 }
139 148
140 void _notifyChange() { 149 void _observeIndex(int i) {
141 if (_scheduled) return; 150 final object = _values[i];
142 _scheduled = true; 151 if (object is Observable) {
152 // TODO(jmesserly): rather than allocating a new closure for each
153 // property, we could try and have one for the entire path. In that case,
154 // we would lose information about which object changed (note: unless
155 // PropertyChangeRecord is modified to includes the sender object), so
156 // we would need to re-evaluate the entire path. Need to evaluate perf.
157 _subs[i] = object.changes.listen((List<ChangeRecord> records) {
158 if (!identical(_values[i], object)) {
159 // Ignore this object if we're now tracking something else.
160 return;
161 }
143 162
144 // TODO(jmesserly): should we have a guarenteed order with respect to other 163 for (var record in records) {
145 // paths? If so, we could implement this fairly easily by sorting instances 164 if (record.changes(_segments[i])) {
146 // of this class by birth order before delivery. 165 _updateObservedValues(i);
147 queueChangeRecords(_deliverValues); 166 return;
148 } 167 }
149 168 }
150 /** Gets the last reported value at this path. */ 169 });
151 get value {
152 if (!_isValid) return null;
153 if (_observer == null) return object;
154 _observer.ensureValue(object);
155 return _lastObserver.value;
156 }
157
158 /** Sets the value at this path. */
159 void set value(Object value) {
160 // TODO(jmesserly): throw if property cannot be set?
161 // MDV seems tolerant of these error.
162 if (_observer == null || !_isValid) return;
163 _observer.ensureValue(object);
164 var last = _lastObserver;
165 if (_setObjectProperty(last._object, last._property, value)) {
166 // Technically, this would get updated asynchronously via a change record.
167 // However, it is nice if calling the getter will yield the same value
168 // that was just set. So we use this opportunity to update our cache.
169 last.value = value;
170 } 170 }
171 } 171 }
172 } 172 }
173 173
174 _getObjectProperty(object, property) { 174 _getObjectProperty(object, property) {
175 if (object is List && property is int) { 175 if (object is List && property is int) {
176 if (property >= 0 && property < object.length) { 176 if (property >= 0 && property < object.length) {
177 return object[property]; 177 return object[property];
178 } else { 178 } else {
179 return null; 179 return null;
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after
213 } 213 }
214 214
215 if (object is Map) { 215 if (object is Map) {
216 object[property] = value; 216 object[property] = value;
217 return true; 217 return true;
218 } 218 }
219 219
220 return false; 220 return false;
221 } 221 }
222 222
223 class _PropertyObserver {
224 final PathObserver _path;
225 final _property;
226 final _PropertyObserver _next;
227
228 // TODO(jmesserly): would be nice not to store both of these.
229 Object _object;
230 Object _value;
231 StreamSubscription _sub;
232
233 _PropertyObserver(this._path, this._property, this._next);
234
235 get value => _value;
236
237 void set value(Object newValue) {
238 _value = newValue;
239 if (_next != null) {
240 if (_sub != null) _next.unobserve();
241 _next.ensureValue(_value);
242 if (_sub != null) _next.observe();
243 }
244 }
245
246 void ensureValue(object) {
247 // If we're observing, values should be up to date already.
248 if (_sub != null) return;
249
250 _object = object;
251 value = _getObjectProperty(object, _property);
252 }
253
254 void observe() {
255 if (_object is Observable) {
256 assert(_sub == null);
257 _sub = (_object as Observable).changes.listen(_onChange);
258 }
259 if (_next != null) _next.observe();
260 }
261
262 void unobserve() {
263 if (_sub == null) return;
264
265 _sub.cancel();
266 _sub = null;
267 if (_next != null) _next.unobserve();
268 }
269
270 void _onChange(List<ChangeRecord> changes) {
271 for (var change in changes) {
272 // TODO(jmesserly): what to do about "new Symbol" here?
273 // Ideally this would only preserve names if the user has opted in to
274 // them being preserved.
275 // TODO(jmesserly): should we drop observable maps with String keys?
276 // If so then we only need one check here.
277 if (change.changes(_property)) {
278 value = _getObjectProperty(_object, _property);
279 _path._notifyChange();
280 return;
281 }
282 }
283 }
284 }
285 223
286 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js 224 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js
287 225
288 const _pathIndentPart = r'[$a-z0-9_]+[$a-z0-9_\d]*'; 226 final _pathRegExp = () {
289 final _pathRegExp = new RegExp('^' 227 const identStart = '[\$_a-zA-Z]';
290 '(?:#?' + _pathIndentPart + ')?' 228 const identPart = '[\$_a-zA-Z0-9]';
291 '(?:' 229 const ident = '$identStart+$identPart*';
292 '(?:\\.' + _pathIndentPart + ')' 230 const elementIndex = '(?:[0-9]|[1-9]+[0-9]+)';
293 ')*' 231 const identOrElementIndex = '(?:$ident|$elementIndex)';
294 r'$', caseSensitive: false); 232 const path = '(?:$identOrElementIndex)(?:\\.$identOrElementIndex)*';
233 return new RegExp('^$path\$');
234 }();
295 235
296 final _spacesRegExp = new RegExp(r'\s'); 236 final _spacesRegExp = new RegExp(r'\s');
297 237
298 bool _isPathValid(String s) { 238 bool _isPathValid(String s) {
299 s = s.replaceAll(_spacesRegExp, ''); 239 s = s.replaceAll(_spacesRegExp, '');
300 240
301 if (s == '') return true; 241 if (s == '') return true;
302 if (s[0] == '.') return false; 242 if (s[0] == '.') return false;
303 return _pathRegExp.hasMatch(s); 243 return _pathRegExp.hasMatch(s);
304 } 244 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698