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

Side by Side Diff: lib/observe.dart

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

Powered by Google App Engine
This is Rietveld 408576698