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

Side by Side Diff: pkg/observe/lib/observe.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 /** 5 /**
6 * *Warning*: this library is experimental, and APIs are subject to change. 6 * *Warning*: this library is experimental, and APIs are subject to change.
7 * 7 *
8 * This library is used to observe changes to [Observable] types. It also 8 * This library is used to observe changes to [Observable] types. It also
9 * has helpers to implement [Observable] objects. 9 * has helpers to make implementing and using [Observable] objects easy.
10 * 10 *
11 * For example: 11 * You can provide an observable object in two ways. The simplest way is to
12 * use dirty checking to discover changes automatically:
12 * 13 *
13 * class Monster extends Unit with ObservableMixin { 14 * class Monster extends Unit with ObservableMixin {
15 * @observable int health = 100;
16 *
17 * void damage(int amount) {
18 * print('$this takes $amount damage!');
19 * health -= amount;
20 * }
21 *
22 * toString() => 'Monster with $health hit points';
23 * }
24 *
25 * main() {
26 * var obj = new Monster();
27 * obj.changes.listen((records) {
28 * print('Changes to $obj were: $records');
29 * });
30 * // No changes are delivered until we check for them
31 * obj.damage(10);
32 * obj.damage(20);
33 * print('dirty checking!');
34 * Observable.dirtyCheck();
35 * print('done!');
36 * }
37 *
38 * A more sophisticated approach is to implement the change notification
39 * manually. This avoids the potentially expensive [Observable.dirtyCheck]
40 * operation, but requires more work in the object:
41 *
42 * class Monster extends Unit with ChangeNotifierMixin {
14 * int _health = 100; 43 * int _health = 100;
15 * get health => _health; 44 * get health => _health;
16 * set health(value) { 45 * set health(val) {
17 * _health = notifyChange(const Symbol('health'), _health, value); 46 * _health = notifyPropertyChange(const Symbol('health'), _health, val);
18 * } 47 * }
19 * 48 *
20 * void damage(int amount) { 49 * void damage(int amount) {
21 * print('$this takes $amount damage!'); 50 * print('$this takes $amount damage!');
22 * health -= amount; 51 * health -= amount;
23 * } 52 * }
24 * 53 *
25 * toString() => 'Monster with $health hit points'; 54 * toString() => 'Monster with $health hit points';
26 * } 55 * }
27 * 56 *
28 * main() { 57 * main() {
29 * var obj = new Monster(); 58 * var obj = new Monster();
30 * obj.changes.listen((records) { 59 * obj.changes.listen((records) {
31 * print('Changes to $obj were: $records'); 60 * print('Changes to $obj were: $records');
32 * }); 61 * });
33 * // Schedules asynchronous delivery of these changes 62 * // Schedules asynchronous delivery of these changes
34 * obj.damage(10); 63 * obj.damage(10);
35 * obj.damage(20); 64 * obj.damage(20);
36 * print('done!'); 65 * print('done!');
37 * } 66 * }
67 *
68 * [Tools](https://github.com/dart-lang/web-ui) exist to convert the first form
69 * into the second form automatically, to get the best of both worlds.
38 */ 70 */
39 library observe; 71 library observe;
40 72
41 import 'dart:async'; 73 import 'dart:async';
42 import 'dart:collection'; 74 import 'dart:collection';
43 import 'dart:mirrors'; 75 import 'dart:mirrors';
44 76
77 // Note: this is an internal library so we can import it from tests.
78 // TODO(jmesserly): ideally we could import this with a prefix, but it caused
79 // strange problems on the VM when I tested out the dirty-checking example
80 // above.
81 import 'src/watcher.dart';
82
83 part 'src/change_notifier.dart';
84 part 'src/change_record.dart';
45 part 'src/compound_binding.dart'; 85 part 'src/compound_binding.dart';
86 part 'src/observable.dart';
46 part 'src/observable_box.dart'; 87 part 'src/observable_box.dart';
47 part 'src/observable_list.dart'; 88 part 'src/observable_list.dart';
48 part 'src/observable_map.dart'; 89 part 'src/observable_map.dart';
49 part 'src/path_observer.dart'; 90 part 'src/path_observer.dart';
50 91 part 'src/to_observable.dart';
51 /**
52 * Interface representing an observable object. This is used by data in
53 * model-view architectures to notify interested parties of [changes].
54 *
55 * This object does not require any specific technique to implement
56 * observability.
57 *
58 * You can use [ObservableMixin] as a base class or mixin to implement this.
59 */
60 abstract class Observable {
61 /**
62 * The stream of change records to this object.
63 *
64 * Changes should be delivered in asynchronous batches by calling
65 * [queueChangeRecords].
66 *
67 * [deliverChangeRecords] can be called to force delivery.
68 */
69 Stream<List<ChangeRecord>> get changes;
70 }
71
72 /**
73 * Base class implementing [Observable].
74 *
75 * When a field, property, or indexable item is changed, a derived class should
76 * call [notifyPropertyChange]. See that method for an example.
77 */
78 typedef ObservableBase = Object with ObservableMixin;
79
80 /**
81 * Mixin for implementing [Observable] objects.
82 *
83 * When a field, property, or indexable item is changed, a derived class should
84 * call [notifyPropertyChange]. See that method for an example.
85 */
86 abstract class ObservableMixin implements Observable {
87 StreamController _broadcastController;
88 List<ChangeRecord> _changes;
89
90 Stream<List<ChangeRecord>> get changes {
91 if (_broadcastController == null) {
92 _broadcastController =
93 new StreamController<List<ChangeRecord>>.broadcast(sync: true);
94 }
95 return _broadcastController.stream;
96 }
97
98 void _deliverChanges() {
99 var changes = _changes;
100 _changes = null;
101 if (hasObservers && changes != null) {
102 // TODO(jmesserly): make "changes" immutable
103 _broadcastController.add(changes);
104 }
105 }
106
107 /**
108 * True if this object has any observers, and should call
109 * [notifyPropertyChange] for changes.
110 */
111 bool get hasObservers => _broadcastController != null &&
112 _broadcastController.hasListener;
113
114 /**
115 * Notify that the field [name] of this object has been changed.
116 *
117 * The [oldValue] and [newValue] are also recorded. If the two values are
118 * identical, no change will be recorded.
119 *
120 * For convenience this returns [newValue]. This makes it easy to use in a
121 * setter:
122 *
123 * var _myField;
124 * get myField => _myField;
125 * set myField(value) {
126 * _myField = notifyPropertyChange(
127 * const Symbol('myField'), _myField, value);
128 * }
129 */
130 // TODO(jmesserly): should this be == instead of identical, to prevent
131 // spurious loops?
132 notifyPropertyChange(Symbol field, Object oldValue, Object newValue) {
133 if (hasObservers && !identical(oldValue, newValue)) {
134 notifyChange(new PropertyChangeRecord(field));
135 }
136 return newValue;
137 }
138
139 /**
140 * Notify observers of a change. For most objects [notifyPropertyChange] is
141 * more convenient, but collections sometimes deliver other types of changes
142 * such as a [ListChangeRecord].
143 */
144 void notifyChange(ChangeRecord record) {
145 if (!hasObservers) return;
146
147 if (_changes == null) {
148 _changes = [];
149 queueChangeRecords(_deliverChanges);
150 }
151 _changes.add(record);
152 }
153 }
154
155
156 /** Records a change to an [Observable]. */
157 abstract class ChangeRecord {
158 /** True if the change affected the given item, otherwise false. */
159 bool change(key);
160 }
161
162 /** A change record to a field of an observable object. */
163 class PropertyChangeRecord extends ChangeRecord {
164 /** The field that was changed. */
165 final Symbol field;
166
167 PropertyChangeRecord(this.field);
168
169 bool changes(key) => key is Symbol && field == key;
170
171 String toString() => '#<PropertyChangeRecord $field>';
172 }
173
174 /** A change record for an observable list. */
175 class ListChangeRecord extends ChangeRecord {
176 /** The starting index of the change. */
177 final int index;
178
179 /** The number of items removed. */
180 final int removedCount;
181
182 /** The number of items added. */
183 final int addedCount;
184
185 ListChangeRecord(this.index, {this.removedCount: 0, this.addedCount: 0}) {
186 if (addedCount == 0 && removedCount == 0) {
187 throw new ArgumentError('added and removed counts should not both be '
188 'zero. Use 1 if this was a single item update.');
189 }
190 }
191
192 /** Returns true if the provided index was changed by this operation. */
193 bool changes(key) {
194 // If key isn't an int, or before the index, then it wasn't changed.
195 if (key is! int || key < index) return false;
196
197 // If this was a shift operation, anything after index is changed.
198 if (addedCount != removedCount) return true;
199
200 // Otherwise, anything in the update range was changed.
201 return key < index + addedCount;
202 }
203
204 String toString() => '#<ListChangeRecord index: $index, '
205 'removed: $removedCount, addedCount: $addedCount>';
206 }
207
208 /**
209 * Synchronously deliver [Observable.changes] for all observables.
210 * If new changes are added as a result of delivery, this will keep running
211 * until all pending change records are delivered.
212 */
213 // TODO(jmesserly): this is a bit different from the ES Harmony version, which
214 // allows delivery of changes to a particular observer:
215 // http://wiki.ecmascript.org/doku.php?id=harmony:observe#object.deliverchangere cords
216 // However the binding system needs delivery of everything, along the lines of:
217 // https://github.com/toolkitchen/mdv/blob/stable/src/model.js#L19
218 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js#L590
219 // TODO(jmesserly): in the future, we can use this to trigger dirty checking.
220 void deliverChangeRecords() {
221 if (_deliverCallbacks == null) return;
222
223 while (!_deliverCallbacks.isEmpty) {
224 var deliver = _deliverCallbacks.removeFirst();
225
226 try {
227 deliver();
228 } catch (e, s) {
229 // Schedule the error to be top-leveled later.
230 new Completer().completeError(e, s);
231 }
232 }
233
234 // Null it out, so [queueChangeRecords] will reschedule this method.
235 _deliverCallbacks = null;
236 }
237
238 /** Queues an action to happen during the [deliverChangeRecords] timeslice. */
239 void queueChangeRecords(void deliverChanges()) {
240 if (_deliverCallbacks == null) {
241 _deliverCallbacks = new Queue<Function>();
242 runAsync(deliverChangeRecords);
243 }
244 _deliverCallbacks.add(deliverChanges);
245 }
246
247 Queue _deliverCallbacks;
248
249
250 /**
251 * Converts the [Iterable] or [Map] to an [ObservableList] or [ObservableMap],
252 * respectively. This is a convenience function to make it easier to convert
253 * literals into the corresponding observable collection type.
254 *
255 * If [value] is not one of those collection types, or is already [Observable],
256 * it will be returned unmodified.
257 *
258 * If [value] is a [Map], the resulting value will use the appropriate kind of
259 * backing map: either [HashMap], [LinkedHashMap], or [SplayTreeMap].
260 *
261 * By default this performs a deep conversion, but you can set [deep] to false
262 * for a shallow conversion. This does not handle circular data structures.
263 * If a conversion is peformed, mutations are only observed to the result of
264 * this function. Changing the original collection will not affect it.
265 */
266 // TODO(jmesserly): ObservableSet?
267 toObservable(value, {bool deep: true}) =>
268 deep ? _toObservableDeep(value) : _toObservableShallow(value);
269
270 _toObservableShallow(value) {
271 if (value is Observable) return value;
272 if (value is Map) return new ObservableMap.from(value);
273 if (value is Iterable) return new ObservableList.from(value);
274 return value;
275 }
276
277 _toObservableDeep(value) {
278 if (value is Observable) return value;
279 if (value is Map) {
280 var result = new ObservableMap._createFromType(value);
281 value.forEach((k, v) {
282 result[_toObservableDeep(k)] = _toObservableDeep(v);
283 });
284 return result;
285 }
286 if (value is Iterable) {
287 return new ObservableList.from(value.map(_toObservableDeep));
288 }
289 return value;
290 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698