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

Unified 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, 11 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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « lib/observe/map.dart ('k') | lib/observe/reference.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: lib/observe/observable.dart
diff --git a/lib/observe/observable.dart b/lib/observe/observable.dart
new file mode 100644
index 0000000000000000000000000000000000000000..12b93599fa0752ef60540acb3e1077345d68cb99
--- /dev/null
+++ b/lib/observe/observable.dart
@@ -0,0 +1,237 @@
+// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/**
+ * A library for observing changes to observable Dart objects.
+ *
+ * Similar in spirit to EcmaScript 6
+ * [Object.observe](http://wiki.ecmascript.org/doku.php?id=harmony:observe).
+ *
+ * See the [observable] annotation and the [Observable.observe] function.
+ */
+// Note: one difference from ES6 Object.observe is that our change batches are
+// tracked on a per-observed expression basis, instead of per-observer basis.
+//
+// We do this because there is no cheap way to store a pointer on a Dart
+// function (Expando uses linear search on the VM: http://dartbug.com/7558).
+// This difference means that a given observer will be called with one batch of
+// changes for each object it is observing.
+//
+// TODO(jmessery): tracking it per-observer is more powerful. It lets one
+// observer get a complete batch of changes for all objects it observes.
+// Maybe we should attempt to implement it? The main downside is overhead
+// in deliverChangesSync -- you need to hash the functions and collect their
+// change lists.
+library web_ui.observe.observable;
+
+import 'package:web_ui/src/observe/impl.dart' as impl;
+import 'package:web_ui/src/linked_list.dart';
+import 'package:web_ui/src/utils.dart' show setImmediate;
+
+// TODO(jmesserly): rename to @observe?
+/**
+ * Use `@observable` to make a class observable. All fields in the class will
+ * be transformed to track changes. The overhead will be minimal unless they are
+ * actually being observed.
+ *
+ * **Note**: the class needs to be processed by `dwc` for this annotation
+ * to have any effect. It is implemented as a Dart-to-Dart compiler transform.
+ */
+const observable = const Object();
+
+/**
+ * An observable object. This is used by data in model-view architectures
+ * to notify interested parties of changes.
+ */
+class Observable {
+ LinkedListSentinel<ChangeObserver> _observers;
+ List<ChangeRecord> _changes;
+
+ // TODO(jmesserly): in the spirit of mirrors, should all of this
+ // implementation be pulled outside of the Observable mixin?
+
+ bool get hasObservers => _observers != null && _observers.next != null;
+
+ /**
+ * Observes this object and delivers asynchronous notifications of changes
+ * to the [observer].
+ *
+ * The field is considered to have changed if the values no longer compare
+ * equal via the equality operator.
+ *
+ * Returns a function that can be used to stop observation.
+ * Calling this makes it possible for the garbage collector to reclaim memory
+ * associated with the observation and prevents further calls to [callback].
+ *
+ * You can force a synchronous change delivery at any time by calling
+ * [deliverChangesSync]. Calling this method if there are no changes has no
+ * effect. If changes are delivered by deliverChangesSync, they will not be
+ * delivered again asynchronously, unless the value is changed again.
+ *
+ * Any errors thrown by [observer] or [comparison] will be caught and sent to
+ * [onObserveUnhandledError].
+ */
+ ChangeUnobserver observe(ChangeObserver observer) {
+ if (_observers == null) _observers = new LinkedListSentinel();
+ var node = _observers.prepend(observer);
+ return node.remove;
+ }
+
+ // Conceptually, these are protected methods.
+
+ void notifyRead(int type, name) {
+ impl.activeObserver.addRead(this, type, name);
+ }
+
+ void notifyChange(int type, name, Object oldValue, Object newValue) {
+ // If this is an assignment (and not insert/remove) then check if
+ // the value actually changed. If not don't signal a change event.
+ // This helps programmers avoid some common cases of cycles in their code.
+ if ((type & (ChangeRecord.INSERT | ChangeRecord.REMOVE)) == 0) {
+ if (oldValue == newValue) return;
+ }
+
+ if (_changedObjects == null) {
+ _changedObjects = [];
+ setImmediate(deliverChangesSync);
+ }
+ if (_changes == null) {
+ _changes = [];
+ _changedObjects.add(this);
+ }
+ _changes.add(new ChangeRecord(type, name, oldValue, newValue));
+ }
+}
+
+/**
+ * True if we are observing reads. This should be checked before calling
+ * [Observable.notifyRead].
+ *
+ * Note: this is used by objects implementing observability.
+ * You should not need to use this if your type is marked `@observable`.
+ */
+bool get observeReads => impl.activeObserver != null;
+
+
+/** Callback fired when an [Observable] changes. */
+typedef void ChangeObserver(List<ChangeRecord> records);
+
+/** A function that unregisters the [ChangeObserver]. */
+typedef void ChangeUnobserver();
+
+/**
+ * Test for equality of two objects. For example [Object.==] and [identical]
+ * are two kinds of equality tests.
+ */
+typedef bool EqualityTest(Object a, Object b);
+
+/** Records a change to the [target] object. */
+class ChangeRecord {
+ // Note: the target object is omitted because it makes it difficult
+ // to proxy change notifications if you're using an observable type to aid
+ // your implementation.
+ // However: if we allow one observer to get batched changes for multiple
+ // objects we'll need to add target.
+
+ // Note: type values were chosen for easy masking in the observable expression
+ // implementation. However in [type] it will only have one value.
+
+ /** [type] denoting set of a field. */
+ static const FIELD = 1;
+
+ /** [type] denoting an in-place update event using `[]=`. */
+ static const INDEX = 2;
+
+ /**
+ * [type] denoting an insertion into a list. Insertions prepend in front of
+ * the given index, so insert at 0 means an insertion at the beginning of the
+ * list. The index will be provided in [name].
+ */
+ static const INSERT = INDEX | 4;
+
+ /** [type] denoting a remove from a list. */
+ static const REMOVE = INDEX | 8;
+
+ /** Whether the change was a [FIELD], [INDEX], [INSERT], or [REMOVE]. */
+ final int type;
+
+ /**
+ * The name that changed. The value depends on the [type] of change:
+ *
+ * - [FIELD]: the field name that was set
+ * - [INDEX], [INSERT], and [REMOVE]: the index that was changed. Note
+ * that indexes can be strings too, for example with an observable map or
+ * set that has string keys.
+ */
+ final name;
+
+ /** The previous value of the member. */
+ final oldValue;
+
+ /** The new value of the member. */
+ final newValue;
+
+ ChangeRecord(this.type, this.name, this.oldValue, this.newValue);
+}
+
+
+/**
+ * A function that handles an [error] given the [stackTrace] and [callback] that
+ * caused the error.
+ */
+typedef void ObserverErrorHandler(error, stackTrace, Function callback);
+
+/**
+ * Callback to intercept unhandled errors in evaluating an observable.
+ * Includes the error, stack trace, and the callback that caused the error.
+ * By default it will use [defaultObserveUnhandledError], which prints the
+ * error.
+ */
+ObserverErrorHandler onObserveUnhandledError = defaultObserveUnhandledError;
+
+/** The default handler for [onObserveUnhandledError]. Prints the error. */
+void defaultObserveUnhandledError(error, trace, callback) {
+ // TODO(jmesserly): using Logger seems better, but by default it doesn't do
+ // anything, which leads to silent errors. Not good!
+ // Ideally we could make this show up as an error in the browser's console.
+ print('web_ui.observe: unhandled error in callback $callback.\n'
+ 'error:\n$error\n\nstack trace:\n$trace');
+}
+
+/** The per-isolate list of changed objects. */
+List<Observable> _changedObjects;
+
+/**
+ * Delivers observed changes immediately. Normally you should not call this
+ * directly, but it can be used to force synchronous delivery, which helps in
+ * certain cases like testing.
+ *
+ * Note: this will continue delivering changes as long as some are pending.
+ */
+void deliverChangesSync() {
+ int iterations = 0;
+ while (_changedObjects != null) {
+ var changedObjects = _changedObjects;
+ _changedObjects = null;
+
+ for (var observable in changedObjects) {
+ // TODO(jmesserly): freeze the "changes" list?
+ // If one observer incorrectly mutates it, it will affect what future
+ // observers see, possibly leading to subtle bugs.
+ // OTOH, I don't want to add a defensive copy here. Maybe a wrapper that
+ // prevents mutation, or a ListBuilder of some sort than can be frozen.
+ var changes = observable._changes;
+ observable._changes = null;
+
+ for (var n = observable._observers.next; n != null; n = n.next) {
+ var observer = n.value;
+ try {
+ observer(changes);
+ } catch (error, trace) {
+ onObserveUnhandledError(error, trace, observer);
+ }
+ }
+ }
+ }
+}
« 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