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 * |
| 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 } |
OLD | NEW |