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 /** | 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(value) { |
17 * _health = notifyChange(const Symbol('health'), _health, value); | 46 * _health = notifyChange(const Symbol('health'), _health, value); |
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 import 'src/watcher.dart' as watcher; | |
79 | |
80 part 'src/change_notifier.dart'; | |
81 part 'src/change_record.dart'; | |
45 part 'src/compound_binding.dart'; | 82 part 'src/compound_binding.dart'; |
83 part 'src/observable.dart'; | |
46 part 'src/observable_box.dart'; | 84 part 'src/observable_box.dart'; |
47 part 'src/observable_list.dart'; | 85 part 'src/observable_list.dart'; |
48 part 'src/observable_map.dart'; | 86 part 'src/observable_map.dart'; |
49 part 'src/path_observer.dart'; | 87 part 'src/path_observer.dart'; |
50 | 88 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()) { | |
Jennifer Messerly
2013/07/19 01:32:58
the global queueChangeRecords/deliverChangeRecords
| |
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 } | |
OLD | NEW |