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

Side by Side Diff: lib/observe/observable.dart

Issue 12096106: work in progress: observable implementation using detailed change records (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 | « lib/observe/map.dart ('k') | lib/observe/reference.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 *
8 * Similar in spirit to EcmaScript 6
9 * [Object.observe](http://wiki.ecmascript.org/doku.php?id=harmony:observe).
10 *
11 * See the [observable] annotation and the [Observable.observe] function.
12 */
13 // Note: one difference from ES6 Object.observe is that our change batches are
14 // tracked on a per-observed expression basis, instead of per-observer basis.
15 //
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 //
21 // TODO(jmessery): tracking it per-observer is more powerful. It lets one
22 // observer get a complete batch of changes for all objects it observes.
23 // Maybe we should attempt to implement it? The main downside is overhead
24 // in deliverChangesSync -- you need to hash the functions and collect their
25 // change lists.
26 library web_ui.observe.observable;
27
28 import 'package:web_ui/src/observe/impl.dart' as impl;
29 import 'package:web_ui/src/linked_list.dart';
30 import 'package:web_ui/src/utils.dart' show setImmediate;
31
32 // TODO(jmesserly): rename to @observe?
33 /**
34 * Use `@observable` to make a class observable. All fields in the class will
35 * be transformed to track changes. The overhead will be minimal unless they are
36 * actually being observed.
37 *
38 * **Note**: the class needs to be processed by `dwc` for this annotation
39 * to have any effect. It is implemented as a Dart-to-Dart compiler transform.
40 */
41 const observable = const Object();
42
43 /**
44 * An observable object. This is used by data in model-view architectures
45 * to notify interested parties of changes.
46 */
47 class Observable {
48 LinkedListSentinel<ChangeObserver> _observers;
49 List<ChangeRecord> _changes;
50
51 // TODO(jmesserly): in the spirit of mirrors, should all of this
52 // implementation be pulled outside of the Observable mixin?
53
54 bool get hasObservers => _observers != null && _observers.next != null;
55
56 /**
57 * Observes this object and delivers asynchronous notifications of changes
58 * to the [observer].
59 *
60 * The field is considered to have changed if the values no longer compare
61 * equal via the equality operator.
62 *
63 * Returns a function that can be used to stop observation.
64 * Calling this makes it possible for the garbage collector to reclaim memory
65 * associated with the observation and prevents further calls to [callback].
66 *
67 * You can force a synchronous change delivery at any time by calling
68 * [deliverChangesSync]. Calling this method if there are no changes has no
69 * effect. If changes are delivered by deliverChangesSync, they will not be
70 * delivered again asynchronously, unless the value is changed again.
71 *
72 * Any errors thrown by [observer] or [comparison] will be caught and sent to
73 * [onObserveUnhandledError].
74 */
75 ChangeUnobserver observe(ChangeObserver observer) {
76 if (_observers == null) _observers = new LinkedListSentinel();
77 var node = _observers.prepend(observer);
78 return node.remove;
79 }
80
81 // Conceptually, these are protected methods.
82
83 void notifyRead(int type, name) {
84 impl.activeObserver.addRead(this, type, name);
85 }
86
87 void notifyChange(int type, name, Object oldValue, Object newValue) {
88 // If this is an assignment (and not insert/remove) then check if
89 // the value actually changed. If not don't signal a change event.
90 // This helps programmers avoid some common cases of cycles in their code.
91 if ((type & (ChangeRecord.INSERT | ChangeRecord.REMOVE)) == 0) {
92 if (oldValue == newValue) return;
93 }
94
95 if (_changedObjects == null) {
96 _changedObjects = [];
97 setImmediate(deliverChangesSync);
98 }
99 if (_changes == null) {
100 _changes = [];
101 _changedObjects.add(this);
102 }
103 _changes.add(new ChangeRecord(type, name, oldValue, newValue));
104 }
105 }
106
107 /**
108 * True if we are observing reads. This should be checked before calling
109 * [Observable.notifyRead].
110 *
111 * Note: this is used by objects implementing observability.
112 * You should not need to use this if your type is marked `@observable`.
113 */
114 bool get observeReads => impl.activeObserver != null;
115
116
117 /** Callback fired when an [Observable] changes. */
118 typedef void ChangeObserver(List<ChangeRecord> records);
119
120 /** A function that unregisters the [ChangeObserver]. */
121 typedef void ChangeUnobserver();
122
123 /**
124 * Test for equality of two objects. For example [Object.==] and [identical]
125 * are two kinds of equality tests.
126 */
127 typedef bool EqualityTest(Object a, Object b);
128
129 /** Records a change to the [target] object. */
130 class ChangeRecord {
131 // Note: the target object is omitted because it makes it difficult
132 // to proxy change notifications if you're using an observable type to aid
133 // your implementation.
134 // However: if we allow one observer to get batched changes for multiple
135 // objects we'll need to add target.
136
137 // Note: type values were chosen for easy masking in the observable expression
138 // implementation. However in [type] it will only have one value.
139
140 /** [type] denoting set of a field. */
141 static const FIELD = 1;
142
143 /** [type] denoting an in-place update event using `[]=`. */
144 static const INDEX = 2;
145
146 /**
147 * [type] denoting an insertion into a list. Insertions prepend in front of
148 * the given index, so insert at 0 means an insertion at the beginning of the
149 * list. The index will be provided in [name].
150 */
151 static const INSERT = INDEX | 4;
152
153 /** [type] denoting a remove from a list. */
154 static const REMOVE = INDEX | 8;
155
156 /** Whether the change was a [FIELD], [INDEX], [INSERT], or [REMOVE]. */
157 final int type;
158
159 /**
160 * The name that changed. The value depends on the [type] of change:
161 *
162 * - [FIELD]: the field name that was set
163 * - [INDEX], [INSERT], and [REMOVE]: the index that was changed. Note
164 * that indexes can be strings too, for example with an observable map or
165 * set that has string keys.
166 */
167 final name;
168
169 /** The previous value of the member. */
170 final oldValue;
171
172 /** The new value of the member. */
173 final newValue;
174
175 ChangeRecord(this.type, this.name, this.oldValue, this.newValue);
176 }
177
178
179 /**
180 * A function that handles an [error] given the [stackTrace] and [callback] that
181 * caused the error.
182 */
183 typedef void ObserverErrorHandler(error, stackTrace, Function callback);
184
185 /**
186 * Callback to intercept unhandled errors in evaluating an observable.
187 * Includes the error, stack trace, and the callback that caused the error.
188 * By default it will use [defaultObserveUnhandledError], which prints the
189 * error.
190 */
191 ObserverErrorHandler onObserveUnhandledError = defaultObserveUnhandledError;
192
193 /** The default handler for [onObserveUnhandledError]. Prints the error. */
194 void defaultObserveUnhandledError(error, trace, callback) {
195 // TODO(jmesserly): using Logger seems better, but by default it doesn't do
196 // anything, which leads to silent errors. Not good!
197 // Ideally we could make this show up as an error in the browser's console.
198 print('web_ui.observe: unhandled error in callback $callback.\n'
199 'error:\n$error\n\nstack trace:\n$trace');
200 }
201
202 /** The per-isolate list of changed objects. */
203 List<Observable> _changedObjects;
204
205 /**
206 * Delivers observed changes immediately. Normally you should not call this
207 * directly, but it can be used to force synchronous delivery, which helps in
208 * certain cases like testing.
209 *
210 * Note: this will continue delivering changes as long as some are pending.
211 */
212 void deliverChangesSync() {
213 int iterations = 0;
214 while (_changedObjects != null) {
215 var changedObjects = _changedObjects;
216 _changedObjects = null;
217
218 for (var observable in changedObjects) {
219 // TODO(jmesserly): freeze the "changes" list?
220 // If one observer incorrectly mutates it, it will affect what future
221 // observers see, possibly leading to subtle bugs.
222 // OTOH, I don't want to add a defensive copy here. Maybe a wrapper that
223 // prevents mutation, or a ListBuilder of some sort than can be frozen.
224 var changes = observable._changes;
225 observable._changes = null;
226
227 for (var n = observable._observers.next; n != null; n = n.next) {
228 var observer = n.value;
229 try {
230 observer(changes);
231 } catch (error, trace) {
232 onObserveUnhandledError(error, trace, observer);
233 }
234 }
235 }
236 }
237 }
OLDNEW
« no previous file with comments | « lib/observe/map.dart ('k') | lib/observe/reference.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698