can-observation-recorder
Specify how to listen to changes in a value being read and record those specifications between two points in time.
Record observables being read and indicate how to listen to changes in a value being read.
Object
can-observation-recorder exports an object with the following methods:
{
start() // Starts recording ObservationRecorder.add calls and
// returns an ObservationRecord representing
// the ObservationRecorder.add calls.
stop() // Stops recording ObservationRecorder.add calls and
// returns an ObservationRecord representing
// the ObservationRecorder.add calls.
add(obj, [key]) // Signal that an observable value was read.
addMany(observations) // Add many observations at once
ignore(fn) // Return a function that when called, prevents
// Any ObservationRecorder.add calls from being noticed
}
can-observation-recorder serves two main purposes.
- It allows observable values, when read, to specify how to listen to changes in the
value being read.
ObservationRecorder.add( map, "value" ); ObservationRecorder.add( simpleValue ); - Record all calls to
ObservationRecorder.addbetween start and stop:ObservationRecorder.start(); ObservationRecorder.add( person, "age" ); ObservationRecorder.add( fullName ); const observationRecord = ObservationRecorder.stop(); observationRecord; //-> { // keyDependencies: Map{ person: Set[age] }, // valueDependencies: Set[fullName] //}
Use Cases
can-observation-recorder is primarily used in two ways:
- When any of CanJS's observable values (like can-define) are read, they MUST call add to let can-observation and other observable behaviors (react-view-model) know to bind to them.
- It is used by can-observation and other observables are able to track all the observables read within a function.
Specifying how to bind when a value is read
All of CanJS's observables MUST call add when an observable value is read. .add(observable [,key]) is called with arguments
representing how to listen to when that value changes.
Depending if the observable represents a single value or key-value pairs, it will callback add with one or more values.
Key-value observables
If an object has observable key-value pairs, and a key was read through its .get method, it might
implement .get as follows:
const observableKeyValues = {
_data: {},
get: function( key ) {
ObservationRecorder.add( this, key );
return this._data[ key ];
}
// ...
};
ObservationRecorder.add(observable, key) called with two arguments indicates that the observable argument's can.onKeyValue will
be called with the key argument. In the case that observableKeyValues.get("prop") was called, can-observation would call:
observableKeyValues[ canSymbol.for( "can.onKeyValue" ) ]( "prop", updateObservation, "notify" );
So not only MUST observables call add, they also must have the corresponding observable symbols implemented. For observableKeyValues, this might look like:
const observableKeyValues = {
// ...
handlers: new KeyTree( [ Object, Object, Array ] ),
[ canSymbol.for( "can.onKeyValue" ) ]: function( key, handler, queue ) {
this.handlers.add( [ key, queue || "mutate", handler ] );
},
[ canSymbol.for( "can.offKeyValue" ) ]: function( key, handler, queue ) {
this.handlers.delete( [ key, queue || "mutate", handler ] );
}
};
can-event-queue has utilities that make this pattern easier. Also checkout can-key-tree and can-queues.
Single Value Observables
If an observable represents a single value, and that value is read through a .value property, it might
implement that property as follows:
const observableValue = {
_value: {},
get value() {
ObservationRecorder.add( this );
return this._value;
}
// ...
};
ObservationRecorder.add(observable) called with one arguments indicates that the observable argument's can.onValue will be called. In the case that observableKeyValues.value was read, can-observation would call:
observableValue[ canSymbol.for( "can.onValue" ) ]( updateObservation, "notify" );
So not only MUST observables call add, they also must have the corresponding observable symbols implemented. For observableValue, this might look like:
const observableValue = {
// ...
handlers: new KeyTree( [ Object, Array ] ),
[ canSymbol.for( "can.onValue" ) ]: function( handler, queue ) {
this.handlers.add( [ queue || "mutate", handler ] );
},
[ canSymbol.for( "can.offValue" ) ]: function( handler, queue ) {
this.handlers.delete( [ queue || "mutate", handler ] );
}
};
can-event-queue has utilities that make this pattern easier. Also checkout can-key-tree and can-queues.
Tracking observables read between two points in time
One of CanJS's best features is that reading values broadcasts through ObservationRecorder.add how to
listen to when those values changes. start and stop
are used to capture that information, allowing utilities to listen to any values read between .start() and .stop().
This is sued by can-observation to make "computed" observables and by react-view-model to re-render and diff React's virtual DOM. Maybe you've got some cool uses too!
Use .start() and .stop() to track all .add(observation [,key] ) calls between two points as follows:
ObservationRecorder.start();
ObservationRecorder.add( observableKeyValues, "propA" );
ObservationRecorder.add( observableKeyValues, "propB" );
ObservationRecorder.add( map, "propC" );
ObservationRecorder.add( observableValue );
ObservationRecorder.add( fullNameCompute );
const observationRecord = ObservationRecorder.stop();
observationRecord would contain the following:
{
keyDependencies: Map{
[observableKeyValues]: Set["propA", "propB"],
[map]: Set["propC"]
},
valueDependencies: Set[observableValue, fullNameCompute]
}
keyDependencies are kept in a Map that maps each observable to a set of the keys recorded for that observable.
valueDependencies are kept in a Set.
start adds a new observation record to the stack each time it is called. Read its docs for more information about what this means.
How it works
can-observation-recorder is a stateful global. We should avoid breaking its API as much as possible.
It keeps a stack of observation records. .start() pushes to that stack. .add() adds values to the
top of the stack. .stop() pops the stack and returns the former top of the stack.
How it works: can-observation and can-observation-recorder
covers how can-observation-recorder works.
How it works: can-observation and can-observation-recorder
covers how can-observation works.