In batman.js, properties automatically track their sources. This is done by tracking all calls to
get when an accessor function is executed.
I hope to cover automatic dependency tracking in batman.js by describing:
- The “source” relationship between properties
- The structure of the tracker stack
- How the tracker stack is used internally by batman.js
Then I will cover several examples of source tracking:
- No depencies
- One dependency
- Nested dependencies
- Parallel dependencies
- Outside dependencies
1 2 3
isOak changes when
species changes. For example:
1 2 3 4 5 6
We can describe the relationship between
species in two ways:
The Source Tracker Stack
The global source tracker stack is an array of arrays:
- Each sub-array is a list of sources for a property whose value is being calculated.
- Each member of a sub-array is a source for that property.
Here’s an example tracker stack:
1 2 3 4 5 6 7 8 9
- The global tracker is an array
- Its members are arrays
- Inside those arrays are sources
- Some properties have no other sources
I’ll be using strings to represent sources, but batman.js actually uses
Batman.Property instances. A
Batman.Property has a
base (usually a
Batman.Object) and a
key, which is the string identifier for the property.
How Batman.js Uses Source Tracker Stack
Internally, batman.js uses the source tracker stack whenever properties are evaluated with
get (if they weren’t already cached).
get functions are wrapped with batman.js’s source tracking:
1 2 3 4 5 6 7 8 9 10
At the beginning each call to
propertyto the current open tracker, if there is one. To determine whether the current
getis called in the context of evaluating another property, batman.js checks for an open tracker (ie, an array inside the global source tracker). If there is one, it pushes the current property as source of whatever property was being evaluated.
- Pushes a new entry in the tracker. Batman.js prepares the source tracker for any dependencies by pushing a child array. If any other properties are accessed, they will be pushed to that child array (via step 1 above!).
get functions finish, batman.js cleans up the source tracker stack by:
- Getting the list of sources by popping off of the global source tracker.
- Creating observers for all sources.
In a property lookup, there are no other calls to
get, so the source tracker doesn’t do very much. Here’s what it would look like if you watched the global source tracker:
1 2 3 4 5 6 7 8
Batman.js prepared to track the sources for
species, but didn’t find any.
The example above, calling
get('isOak') causes batman.js to calculate the tree’s
Here’s what the tracker stack would look like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
Batman.js handles nested calls to
get by pushing entries to the source tracker. When the nested class resolve, entries are popped back off the source tracker.
For example, let’s add another property that depends on
1 2 3
hasAcorns’s only source is
isOak. The dependency chain looks like this:
So, here’s what the source tracker stack looks like for calculating
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Note: Batman.js only evaluates properties that aren’t cached, so you don’t have to worry about “abusing” deeply nested properties.
Properties may also have multiple, non-nested sources. These are parallel sources:
1 2 3
description depends on
species. If either one changes, the property will be reevaluated.
description is calculated, it will register
species as sources. Here’s what it would look like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Dependencies on Other Objects
So far, all examples have used
@get inside accessors. However, it’s safe to access properties of any
get inside an accessor function. This is because the
Batman.Property is aware of its base and key. Base is the object that the property belongs to and key is the string name of the property. When you use
get on another object, the correct object and property are tracked as sources.
Tree::ownerName depends on an outside object (a
1 2 3 4 5 6 7
In this case
owner.get('name') registers a
Batman.Property whose base is a
Person. If that person’s name changes,
ownerName will be reevaluated.
Let’s add a property to
Tree that has some conditional logic.
Tree::bestAvailableFood contains conditional branching:
1 2 3 4 5 6 7 8
Batman.js will only track calls to
get that are actually executed, so if
hasFruit returns true, then
hasAcorns won’t be registered as a source.
hasAcorns changes? It doesn’t matter – the property would still evaluate to
"fruit" (from the
hasFruit branch), so batman.js saved itself some trouble!
hasAcorns both returned false, they would both be registered as sources (as in the “parallel sources” example). The property would be reevaluated if either one changed.
Iteration is safe inside accessor bodies as long as you play by batman.js’s rules:
- Enumerables must extend
- Enumerables must be retrieved with
getso that a wholesale replacement of the enumerable is observed, too.
Let’s look at two accessors that have iteration in their
get functions: one has an early return, the always visits each member of the set.
These accessors could be simplifed by using
Batman.Enumerable functions, but they’re spelled out for clarity’s sake!
Tree::hasFruit returns as soon as it finds a limb with fruit:
1 2 3 4 5 6 7
limbs and each
limb.hasFruit will be added as sources, until a
limb.hasFruit returns true.
Some limbs won’t be observed as sources, but that’s OK: the property will be true as long as the first true
limb.hasFruit still evaluates to true. If that first
false, the property will be reevaluated.
Similarly, if one of the earlier limbs becomes
true, the property will be reevaluated. (And in that case, it will register fewer sources, since it made fewer iterations before finding a
Depends on Every Member
Tree::totalFruits is the sum of fruits on all limbs, so it must observe every limb:
1 2 3 4 5
Since every limb will be visited during evaluation, every limb will be added as a source. Whenever one of the
limb.fruits.length changes, the property will be reevaluated.