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
- Conditionals
- Iteration
Sources/Dependencies
Consider Tree:
class Tree extends Batman.Object
@accessor 'species'
@accessor 'isOak', -> @get('species') is 'oak'
A Tree’s isOak changes when species changes. For example:
shadeTree = new Tree(species: 'maple')
shadeTree.get('isOak')
# => false
shadeTree.set('species', 'oak')
shadeTree.get('isOak')
# => true
We can describe the relationship between isOak and species in two ways:
isOakdepends onspeciesspeciesisisOak’s source
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:
[
[
<Batman.Property "species">,
<Batman.Property "age">
],
[
# no sources
]
]
- 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:
┌────────────────────────────────────────────────────
│ -> Property is pushed to open tracker,
│ if there is one
│ -> Batman.js opens the stack for sources
│ ┌─────────────────────────────────────────────────
│ │ -> Accessor function is executed
│ │ and returns a value
│ └─────────────────────────────────────────────────
│ -> Batman.js registers sources
└────────────────────────────────────────────────────
At the beginning each call to get("property"), batman.js:
- Adds
propertyto the current open tracker, if there is one. To determine whether the currentgetis 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!).
When 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.
No Dependencies
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:
# Call stack # Source tracker stack
# []
shadeTree.get('species') # []
# there is no entry in the stack to add `species` to.
# batman.js pushes an entry for `species`'s sources
# [ [] ]
-> return 'oak' # [ [] ]
# batman.js registers sources (none!) and clears the tracker
Batman.js prepared to track the sources for species, but didn’t find any.
One Dependency
The example above, calling get('isOak') causes batman.js to calculate the tree’s isOak value.
Here’s what the tracker stack would look like:
# Call stack # Source tracker stack
# []
shadeTree.get('isOak') # []
# there is no entry in the stack to add `isOak` to.
# batman.js pushes an entry in the source tracker for `isOak`'s sources
# [ [] ]
-> @get('species')
# batman.js adds `species` to `isOak`'s sources
# [ [species] ]
# batman.js pushes an entry in the source tracker for `species`
# [ [species], [] ]
-> return # [ [species], [] ]
# batman.js pops `species`'s sources -- but there weren't any
-> is 'oak' # [ [species] ]
-> # batman.js pops `isOak`'s dependencies and registers them internally
-> return # []
Deeply-Nested Dependencies
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 isOak:
class Tree extends Batman.Object
# ...
@accessor 'hasAcorns', -> @get('isOak')
hasAcorns’s only source is isOak. The dependency chain looks like this:
species -> isOak -> hasAcorns
So, here’s what the source tracker stack looks like for calculating hasAcorns:
# Call stack # Source tracker stack
# []
shadeTree.get('hasAcorns')
# [ [] ]
-> @get('isOak')
# [ [isOak], [] ]
-> @get('species') # [ [isOak], [species], [] ]
-> return
# batman.js doesn't register any sources for `species`
# [ [isOak], [species] ]
-> return
# batman.js registers `isOak`'s source, `species`
# [ [isOak] ]
-> return
# batman.js registers `hasAcorn`'s source, `isOak`
Note: Batman.js only evaluates properties that aren’t cached, so you don’t have to worry about “abusing” deeply nested properties.
Parallel Dependencies
Properties may also have multiple, non-nested sources. These are parallel sources:
class Tree extends Batman.Object
@accessor 'description', ->
"#{@get('age')}-year-old #{@get('species')}"
description depends on age and species. If either one changes, the property will be reevaluated.
When description is calculated, it will register age and species as sources. Here’s what it would look like:
# Call stack # Source tracker stack
# []
shadeTree.get('description')
# [ [] ]
-> @get('age')
# [ [age] ]
# [ [age], [] ]
-> return
# [ [age] ]
-> @get('species')
# [ [age, species] ]
# [ [age, species], [] ]
-> return
# [ [age, species] ]
->
# batman.js registers both sources
# []
Dependencies on Other Objects
So far, all examples have used @get inside accessors. However, it’s safe to access properties of any Batman.Object with 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.
For example, Tree::ownerName depends on an outside object (a Person object):
class Tree extends Batman.Object
# ...
@accessor 'ownerName', ->
ownerId = @get('ownerId')
# find owner by id:
owner = Person.get('all').indexedByUnique('id').get(ownerId)
owner.get('name')
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.
Conditionals
Let’s add a property to Tree that has some conditional logic. Tree::bestAvailableFood contains conditional branching:
class Tree extends Batman.Object
@accessor 'bestAvailableFood', ->
if @get('hasFruit')
"fruit"
else if @get('hasAcorns')
"acorns"
else
null
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.
What if hasAcorns changes? It doesn’t matter – the property would still evaluate to "fruit" (from the hasFruit branch), so batman.js saved itself some trouble!
If hasFruit and 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
Iteration is safe inside accessor bodies as long as you play by batman.js’s rules:
- Enumerables must extend
Batman.Objectso that they’re observable. Plain JavaScript Arrays and Objects can’t be registered as sources. - 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!
Early Return
Tree::hasFruit returns as soon as it finds a limb with fruit:
class Tree extends Batman.Object
@accessor 'limbs' # has a Batman.Set
@accessor 'hasFruit', ->
@get('limbs').forEach (limb) ->
return true if limb.get('hasFruit')
false
During evaluation, 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 limb.hasFruit becomes 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 true value.)
Depends on Every Member
Tree::totalFruits is the sum of fruits on all limbs, so it must observe every limb:
@accessor 'totalFruits', ->
totalCount = 0
@get('limbs').forEach (limb) ->
totalCount += limb.get('fruits.length') || 0
totalcount
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.