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

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