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

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
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 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 }
OLDNEW
« no previous file with comments | « example/todomvc/router_options.html ('k') | lib/observe/html.dart » ('j') | lib/observe/map.dart » ('J')

Powered by Google App Engine
This is Rietveld 408576698