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:
isOak
depends onspecies
species
isisOak
’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
property
to the current open tracker, if there is one. To determine whether the currentget
is 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.Object
so that they’re observable. Plain JavaScript Arrays and Objects can’t be registered as sources. - Enumerables must be retrieved with
get
so 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.