OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
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. | |
4 | |
5 /** | |
6 * A library for observing changes to observable Dart objects. | |
7 * Similar in spirit to EcmaScript 6 | |
Siggi Cherem (dart-lang)
2013/02/13 01:43:24
6 => 7
(and below)
Jennifer Messerly
2013/02/13 05:43:15
yeah. Or maybe "EcmaScript Harmony"--to match the
| |
8 * [Object.observe](http://wiki.ecmascript.org/doku.php?id=harmony:observe), but | |
9 * able to observe expressions and not just objects, so long as the expressions | |
10 * are computed from observable objects. | |
11 * | |
12 * See the [observable] annotation and the [observe] function. | |
13 */ | |
14 // Note: one intentional difference from ES6 Object.observe is that our change | |
15 // batches are tracked on a per-observed expression basis, instead of per-observ er basis. | |
Siggi Cherem (dart-lang)
2013/02/13 01:43:24
long line
Jennifer Messerly
2013/02/13 05:43:15
Done.
| |
16 // We do this because there is no cheap way to store a pointer on a Dart | |
17 // function (Expando uses linear search on the VM: http://dartbug.com/7558). | |
18 // This difference means that a given observer will be called with one batch of | |
19 // changes for each object it is observing. | |
20 library observe; | |
21 | |
22 import 'dart:collection'; | |
23 import 'src/utils.dart' show setImmediate; | |
Siggi Cherem (dart-lang)
2013/02/13 01:43:24
funny, I never considered using 'src/utils.dart' f
Jennifer Messerly
2013/02/13 05:43:15
Agreed, I hope our version goes away soon. Added T
| |
24 | |
25 // TODO(jmesserly): support detailed change records on our collections, such as | |
26 // INSERT/REMOVE, so we can use them from templates. Unlike normal objects, | |
27 // list/map/set can add or remove new observable things at runtime, so it's | |
28 // important to provide a way to listen for that. | |
29 export 'observe/list.dart'; | |
30 export 'observe/map.dart'; | |
31 export 'observe/reference.dart'; | |
32 export 'observe/set.dart'; | |
33 | |
34 // TODO(jmesserly): notifyRead/notifyWrite are only used by people | |
35 // implementating advanced observable functionality. The need to be public, but | |
Siggi Cherem (dart-lang)
2013/02/13 01:43:24
The => they
Jennifer Messerly
2013/02/13 05:43:15
Done. Good catch!
| |
36 // ideally they would not be in the top level "observe" library. | |
37 | |
38 /** | |
39 * Use `@observable` to make a class observable. All fields in the class will | |
40 * be transformed to track changes. The overhead will be minimal unless they are | |
41 * actually being observed. | |
42 */ | |
43 const observable = const Object(); | |
44 | |
45 /** Callback fired when an expression changes. */ | |
46 typedef void ChangeObserver(ChangeNotification e); | |
47 | |
48 /** A function that unregisters the [ChangeObserver]. */ | |
49 typedef void ChangeUnobserver(); | |
50 | |
51 /** A function that computes a value. */ | |
52 typedef Object ObservableExpression(); | |
53 | |
54 /** | |
55 * A function that handles an [error] given the [stackTrace] and [callback] that | |
56 * caused the error. | |
57 */ | |
58 typedef void ObserverErrorHandler(error, stackTrace, Function callback); | |
59 | |
60 /** | |
61 * Test for equality of two objects. For example [Object.==] and [identical] | |
62 * are two kinds of equality tests. | |
63 */ | |
64 typedef bool EqualityTest(Object a, Object b); | |
65 | |
66 /** | |
67 * A notification of a change to an [ObservableExpression] that is passed to a | |
68 * [ChangeObserver]. | |
69 */ | |
70 class ChangeNotification { | |
71 | |
72 /** Previous value seen on the watched expression. */ | |
73 final oldValue; | |
74 | |
75 /** New value seen on the watched expression. */ | |
76 final newValue; | |
77 | |
78 ChangeNotification(this.oldValue, this.newValue); | |
79 | |
80 // Note: these two methods are here mainly to make testing easier. | |
81 bool operator ==(other) { | |
82 return other is ChangeNotification && oldValue == other.oldValue && | |
83 newValue == other.newValue; | |
84 } | |
85 | |
86 String toString() => 'change from $oldValue to $newValue'; | |
87 } | |
88 | |
89 /** | |
90 * Observes the [expression] and delivers asynchronous notifications of changes | |
91 * to the [callback]. | |
92 * | |
93 * The expression is considered to have changed if the values no longer compare | |
94 * equal via the equality operator. You can perform additional comparisons in | |
95 * the [callback] if desired. | |
96 * | |
97 * Returns a function that can be used to stop observation. | |
98 * Calling this makes it possible for the garbage collector to reclaim memory | |
99 * associated with the observation and prevents further calls to [callback]. | |
100 * | |
101 * Because notifications are delivered asynchronously and batched, only a single | |
102 * notification is provided for all changes that were made prior to running | |
103 * callback. Intermediate values of the expression are not saved. Instead, | |
104 * [ChangeNotification.oldValue] represents the value before any changes, and | |
105 * [ChangeNotification.newValue] represents the current value of [expression] | |
106 * at the time that [callback] is called. | |
107 * | |
108 * You can force a synchronous change delivery at any time by calling | |
109 * [deliverChangesSync]. Calling this method if there are no changes has no | |
110 * effect. If changes are delivered by deliverChangesSync, they will not be | |
111 * delivered again asynchronously, unless the value is changed again. | |
112 * | |
113 * Any errors thrown by [expression] and [callback] will be caught and sent to | |
114 * [onObserveUnhandledError] | |
Siggi Cherem (dart-lang)
2013/02/13 01:43:24
add '.' at the end of the line
Jennifer Messerly
2013/02/13 05:43:15
Done.
| |
115 */ | |
116 // TODO(jmesserly): debugName is here to workaround http://dartbug.com/8419. | |
117 ChangeUnobserver observe(ObservableExpression expression, | |
118 ChangeObserver callback, [String debugName]) { | |
119 | |
120 var observer = new _ExpressionObserver(expression, callback, debugName); | |
121 if (!observer._observe()) { | |
122 // If we didn't actually read anything, return a pointer to a no-op | |
123 // function so the observer can be reclaimed immediately. | |
124 return _doNothing; | |
125 } | |
126 | |
127 return observer._unobserve; | |
128 } | |
129 | |
130 // Optimizations to avoid extra work if observing const/final data. | |
131 void _doNothing() {} | |
132 | |
133 /** | |
134 * The current observer that is tracking reads, or null if we aren't tracking | |
135 * reads. Reads are tracked when executing [_ExpressionObserver._observe]. | |
136 */ | |
137 _ExpressionObserver _activeObserver; | |
138 | |
139 /** | |
140 * True if we are observing reads. This should be checked before calling | |
141 * [notifyRead]. | |
142 * | |
143 * Note: this type is used by objects implementing observability. | |
144 * You should not need it if your type is marked `@observable`. | |
145 */ | |
146 bool get observeReads => _activeObserver != null; | |
147 | |
148 /** | |
149 * Notify the system of a new read. This will add the current change observer | |
150 * to the set of observers for this field. This should *only* be called when | |
151 * [observeReads] is true, and it will initialize [observers] if it is null. | |
152 * For example: | |
153 * | |
154 * get foo { | |
155 * if (observeReads) _fooObservers = notifyRead(_fooObservers); | |
156 * return _foo; | |
157 * } | |
158 * | |
159 * Note: this function is used to implement observability. | |
160 * You should not need it if your type is marked `@observable`. | |
161 * | |
162 * See also: [notifyWrite] | |
163 */ | |
164 Object notifyRead(fieldObservers) { | |
Siggi Cherem (dart-lang)
2013/02/13 01:43:24
maybe document the possible types for fieldObserve
Jennifer Messerly
2013/02/13 05:43:15
Added comment inside the method. I intentionally d
| |
165 _activeObserver._wasRead = true; | |
166 | |
167 // Note: there's some optimization here to avoid allocating an observer list | |
168 // unless we really need it. | |
169 if (fieldObservers == null) { | |
170 return _activeObserver; | |
171 } | |
172 if (fieldObservers is _ExpressionObserver) { | |
173 if (identical(fieldObservers, _activeObserver) || fieldObservers._dead) { | |
174 return _activeObserver; | |
175 } | |
176 return [fieldObservers, _activeObserver]; | |
177 } | |
Siggi Cherem (dart-lang)
2013/02/13 01:43:24
maybe assert (fieldObservers is List)?
Jennifer Messerly
2013/02/13 05:43:15
Trying to avoid anything here that will make this
| |
178 return fieldObservers..add(_activeObserver); | |
179 } | |
180 | |
181 /** | |
182 * Notify the system of a new write. This will deliver a change notification | |
183 * to the set of observers for this field. This should *only* be called for a | |
184 * non-null list of [observers]. For example: | |
Siggi Cherem (dart-lang)
2013/02/13 01:43:24
would it be sensible to accept a null list and ret
Jennifer Messerly
2013/02/13 05:43:15
It's faster to avoid the call in the common case w
| |
185 * | |
186 * set foo(value) { | |
187 * if (_fooObservers != null && _foo != value) { | |
188 * _fooObservers = notifyWrite(_fooObservers); | |
189 * } | |
190 * _foo = value; | |
191 * } | |
192 * | |
193 * Note: this function is used to implement observability. | |
194 * You should not need it if your type is marked `@observable`. | |
195 * | |
196 * See also: [notifyRead] | |
197 */ | |
198 Object notifyWrite(Object fieldObservers) { | |
199 if (_pendingWrites == null) { | |
200 _pendingWrites = []; | |
201 setImmediate(deliverChangesSync); | |
202 } | |
203 _pendingWrites.add(fieldObservers); | |
204 | |
205 // Clear fieldObservers. This will prevent a second notification for this | |
206 // same set of observers on the current event loop. It also frees associated | |
207 // memory. If the item needs to be observed again, that will happen in | |
208 // _ExpressionObserver._deliver. | |
209 | |
210 // NOTE: ObservableMap depends on this returning null. | |
211 return null; | |
212 } | |
213 | |
214 List _pendingWrites; | |
215 | |
216 /** | |
217 * The limit of times we will attempt to deliver a set of pending changes. | |
218 * | |
219 * [deliverChangesSync] will attempt to deliver pending changes until there are | |
220 * no more. If one of the pending changes causes another batch of changes, it | |
221 * will iterate again and increment the iteration counter. Once it reaches | |
222 * this limit it will throw [CircularChangeNotifyError]. | |
223 * | |
224 * Note that there is no limit to the number of changes per batch, only to the | |
225 * number of iterations. | |
226 */ | |
227 int circularNotificationLimit = 100; | |
Siggi Cherem (dart-lang)
2013/02/13 01:43:24
100 feels big, but maybe because in watchers 10 fe
Jennifer Messerly
2013/02/13 05:43:15
Watchers are very different :). It meant you're co
| |
228 | |
229 /** | |
230 * Delivers observed changes immediately. Normally you should not call this | |
231 * directly, but it can be used to force synchronous delivery, which helps in | |
232 * certain cases like testing. | |
233 */ | |
234 void deliverChangesSync() { | |
235 int iterations = 0; | |
236 while (_pendingWrites != null) { | |
237 var pendingWrites = _pendingWrites; | |
238 _pendingWrites = null; | |
239 | |
240 // Sort pending observers by order added. | |
241 // TODO(jmesserly): this is here to help our template system, which relies | |
242 // on earlier observers removing later ones to prevent them from firing. | |
243 // See if we can find a better solution at the template level. | |
244 var pendingObservers = new SplayTreeMap<int, _ExpressionObserver>(); | |
245 for (var pending in pendingWrites) { | |
246 if (pending is _ExpressionObserver) { | |
247 pendingObservers[pending._id] = pending; | |
248 } else { | |
249 for (var observer in pending) { | |
250 pendingObservers[observer._id] = observer; | |
251 } | |
252 } | |
253 } | |
254 | |
255 if (iterations++ == circularNotificationLimit) { | |
256 _diagnoseCircularLimit(pendingObservers); | |
257 } | |
258 | |
259 // TODO(jmesserly): we are avoiding SplayTreeMap.values because it performs | |
260 // an unnecessary copy. If that gets fixed we can use .values here. | |
Siggi Cherem (dart-lang)
2013/02/13 01:43:24
should we file a bug?
Jennifer Messerly
2013/02/13 05:43:15
Done.
| |
261 pendingObservers.forEach((id, obs) { obs._deliver(); }); | |
262 } | |
263 } | |
264 | |
265 /** | |
266 * Attempt to provide diagnostics about what change is causing a loop in | |
267 * observers. Unfortunately it is hard to help the programmer unless they have | |
268 * provided a `debugName` to [observe], as callbacks are hard to debug | |
269 * because of <http://dartbug.com/8419>. However we can print the records that | |
270 * changed which has proved helpful. | |
271 */ | |
272 void _diagnoseCircularLimit(Map<int, _ExpressionObserver> pendingObservers) { | |
Siggi Cherem (dart-lang)
2013/02/13 01:43:24
would it also be possible to detect circularity du
Jennifer Messerly
2013/02/13 05:43:15
Interesting idea. Essentially, a runtime check for
Siggi Cherem (dart-lang)
2013/02/13 19:28:54
Yeah - basically I think of it as a combination of
Jennifer Messerly
2013/02/13 20:21:20
Yeah, I think the challenge is the cycles don't ne
| |
273 var trace = new StringBuffer('exceeded notifiction limit of ' | |
274 '${circularNotificationLimit}, possible ' | |
275 'circular reference in observers: '); | |
276 | |
277 int i = 0; | |
278 pendingObservers.forEach((id, obs) { | |
279 if (i < 10) { | |
280 if (i != 0) trace.add(', '); | |
281 var change = obs._deliver(); | |
282 trace.add('$obs ${change != null ? change : "no change"}'); | |
283 } else { | |
284 obs._deliver(); | |
285 } | |
286 | |
287 i++; | |
Siggi Cherem (dart-lang)
2013/02/13 01:43:24
consider only incrementing and printing the top-10
Jennifer Messerly
2013/02/13 05:43:15
Great idea. Done.
| |
288 }); | |
289 | |
290 // Throw away pending changes to prevent repeating this error. | |
291 _pendingWrites = null; | |
292 | |
293 // TODO(jmesserly): should we print or log this instead? | |
294 // We throw an error because it probably means a bug in the program structure. | |
Siggi Cherem (dart-lang)
2013/02/13 01:43:24
In watchers it was nice that we kept going - a bug
Jennifer Messerly
2013/02/13 05:43:15
Not sure you really want the application to keep g
| |
295 throw new CircularChangeNotifyError(trace.toString()); | |
296 } | |
297 | |
298 | |
299 /** | |
300 * Error thrown when change notifications get stuck in a circular loop, which | |
301 * can happen if one [ChangeObserver] causes another change to happen, and that | |
302 * change causes another, etc. | |
303 * | |
304 * This is thrown when [circularNotificationLimit] is reached by | |
305 * [deliverChangesSync]. Circular references are commonly the result of not | |
306 * correctly implementing equality for objects. | |
307 */ | |
308 class CircularChangeNotifyError implements Error { | |
309 final String message; | |
310 CircularChangeNotifyError(this.message); | |
311 String toString() => message; | |
312 } | |
313 | |
314 | |
315 class _ExpressionObserver { | |
316 static int _nextId = 0; | |
317 | |
318 /** | |
319 * The ID indicating creation order. We will call observers in ID order. | |
320 * See the TODO in [deliverChangesSync]. | |
321 */ | |
322 final int _id = ++_ExpressionObserver._nextId; | |
323 | |
324 // Note: fields in this class are private because instances of this class are | |
325 // exposed via notifyRead. | |
326 ObservableExpression _expression; | |
327 | |
328 ChangeObserver _callback; | |
329 | |
330 // TODO(jmesserly): should we provide the old value? This will keep it | |
331 // alive until we have a new value. Watchers needed to keep it alive anyway, | |
332 // but we don't need it in observers, except to pass it to the ChangeObserver. | |
Siggi Cherem (dart-lang)
2013/02/13 01:43:24
In that case, wouldn't an event get trigger as soo
Jennifer Messerly
2013/02/13 05:43:15
Good point. Removed comment!
| |
333 Object _value; | |
334 | |
335 bool _wasRead; | |
336 | |
337 String _debugName; | |
338 | |
339 _ExpressionObserver(this._expression, this._callback, this._debugName); | |
340 | |
341 /** True if this observer has been unobserved. */ | |
342 // Note: any time we call out to user-provided code, they might call | |
343 // unobserve, so we need to guard against that. | |
344 bool get _dead => _callback == null; | |
345 | |
346 String toString() => | |
347 _debugName != null ? '<observer $_id: $_debugName>' : '<observer $_id>'; | |
348 | |
349 bool _observe() { | |
350 // If an observe call starts another observation, we need to make sure that | |
351 // the outer observe is tracked correctly. | |
352 var parent = _activeObserver; | |
353 _activeObserver = this; | |
354 | |
355 _wasRead = false; | |
356 try { | |
357 _value = _expression(); | |
358 } catch (e, trace) { | |
359 onObserveUnhandledError(e, trace, _expression); | |
360 _value = null; | |
361 } | |
362 | |
363 // TODO(jmesserly): should the parent also observe us? | |
364 assert(_activeObserver == this); | |
365 _activeObserver = parent; | |
366 | |
367 return _wasRead; | |
368 } | |
369 | |
370 void _unobserve() { | |
371 if (_dead) return; | |
372 | |
373 // Note: we don't remove ourselves from objects that we are observing. | |
374 // That will happen automatically when those fields are written. | |
375 // Instead, we release our own memory and wait for notifyWrite and | |
376 // deliverChangesSync to do the rest. | |
377 // TODO(jmesserly): this is probably too over-optimized. We'll need to | |
378 // revisit this to provide detailed change records. | |
379 _expression = null; | |
380 _callback = null; | |
381 _value = null; | |
382 _wasRead = null; | |
383 _debugName = null; | |
384 } | |
385 | |
386 /** | |
387 * _deliver does two things: | |
388 * 1. Evaluate the expression to compute the new value. | |
389 * 2. Invoke observer for this expression. | |
390 * | |
391 * Note: if you mutate a shared value from one observer, future | |
392 * observers will see the updated value. Essentially, we collapse | |
393 * the two change notifications into one. | |
394 * | |
395 * We could split _deliver into two methods, one to compute the new value | |
396 * and another to call observers. But the current order has benefits too: it | |
397 * preserves the invariant that ChangeNotification.newValue equals the current | |
398 * value of the expression. | |
399 */ | |
400 ChangeNotification _deliver() { | |
401 if (_dead) return null; | |
402 | |
403 // Call the expression again to compute the new value, and to get the new | |
404 // list of dependencies. | |
405 var oldValue = _value; | |
406 _observe(); | |
407 | |
408 if (_dead) return null; | |
Siggi Cherem (dart-lang)
2013/02/13 01:43:24
can _dead change in _observe?, can _dead change in
Jennifer Messerly
2013/02/13 05:43:15
added comment:
// Note: whenever we run code
| |
409 | |
410 bool equal; | |
411 try { | |
412 equal = oldValue == _value; | |
413 } catch (e, trace) { | |
414 onObserveUnhandledError(e, trace, null); | |
415 return; | |
416 } | |
417 | |
418 if (equal || _dead) return null; | |
419 | |
420 var change = new ChangeNotification(oldValue, _value); | |
421 try { | |
422 _callback(change); | |
423 } catch (e, trace) { | |
424 onObserveUnhandledError(e, trace, _callback); | |
425 } | |
426 return change; | |
427 } | |
428 | |
429 // TODO(jmesserly): workaround for terrible VM hash code performance. | |
430 int get hashCode => _id; | |
431 } | |
432 | |
433 | |
434 /** | |
435 * Callback to intercept unhandled errors in evaluating an observable. | |
436 * Includes the error, stack trace, and the callback that caused the error. | |
437 * By default it will use [defaultObserveUnhandledError], which prints the | |
438 * error. | |
439 */ | |
440 ObserverErrorHandler onObserveUnhandledError = defaultObserveUnhandledError; | |
441 | |
442 /** The default handler for [onObserveUnhandledError]. Prints the error. */ | |
443 void defaultObserveUnhandledError(error, trace, callback) { | |
444 // TODO(jmesserly): using Logger seems better, but by default it doesn't do | |
445 // anything, which leads to unobserved errors. | |
446 // Ideally we could make this show up as an error in the browser's console. | |
447 print('web_ui.observe: unhandled error in callback $callback.\n' | |
448 'error:\n$error\n\nstack trace:\n$trace'); | |
449 } | |
OLD | NEW |