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