I received a question over email asking about how Knockout’s dependency detection actually works and thought that I would share an answer in this post. I know that I feel a bit uncomfortable whenever a library that I am using does something that I don’t fully understand, so I hope that I can help ensure that this part of Knockout is not misunderstood or considered “magic”.
A few questions to answer:
- For a computed observable, how does KO know which dependencies should trigger a re-evaluation of the computed on changes?
- How is it possible for the dependencies to change each time that a computed is evaluated?
- For bindings, how are dependencies tracked?
Determining dependencies for a computed
TLDR: Knockout has a middle-man object that is signalled on all reads to computed/observables and tells the current computed being evaluated that it might want to hook up a subscription to this dependency.
Internally Knockout maintains a single object (
ko.dependencyDetection
) that acts as the mediator between parties interested in subscribing to dependencies and dependencies that are being accessed. Let’s call this object the dependency tracker.In a block of code that wants to track dependencies (like in a computed’s evaluation), a call is made to the dependency tracker to signal that someone is currently interested in dependencies. As an example, let’s simulate what a computed would call:
1 2 3 4 5 6 7 8 9 10 11 |
|
- Any read to an observable or computed triggers a call to the dependency tracker. A unique id is then assigned to the observable/computed (if it doesn’t have one) and the callback from the currently interested party is passed the dependency and its id.
1 2 3 4 5 |
|
- The dependency tracker maintains a stack of interested parties. Whenever a call is made to start tracking, a new context is pushed onto the stack, which allows for computeds to be created inside of computeds. Any reads to dependencies will go to the current computed being evaluated.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
- When a computed is done evaluating, it signals the dependency tracker that is is complete and the tracker pops the context off of its stack and restores the previous context.
1 2 3 4 5 6 7 8 9 10 11 |
|
- When an observable/computed dependency is updated, then the subscription is triggered and the computed is re-evaluated.
Here is a jsFiddle version of this code: http://jsfiddle.net/rniemeyer/F9CrA/. Note that ko.dependencyDetection
is only exposed in the debug build. In the release build it is renamed as part of the minification process.
So, Knockout doesn’t need to parse the function as a string to determine dependencies or do any “tricks” to make this happen. The key is that all reads to observable/computeds go through logic that is able to signal the dependency tracker who can let the computed know to subscribe to the observable/computed.
How can dependencies change when a computed is re-evaluated?
Each time that a computed is evaluated, Knockout determines the dependencies again. For any new dependencies, a subscription is added. For any dependencies that are no longer necessary, the subscriptions are disposed. Generally, this is efficient and beneficial as long as you are only branching in computed code based either data that is observable or doesn’t change. For example:
1 2 3 4 5 6 7 8 9 10 11 |
|
In this example, when showErrors
is false, this computed will only have a single dependency, showErrors
. There is no need to depend on the history or trigger re-evaluation when the items change, as it will not influence the result of the function. If showErrors
does become truthy, then the computed will depend on showErrors
, the history
observableArray, and the type
of each item.
What about bindings? How do they track dependencies?
Bindings in Knockout actually use computeds as a tool to facilitate their own dependency tracking. Each binding is evaluated within a computed observable for this purpose. Observables/computeds accessed in the update
function of a binding become dependencies. Observables that are accessed within a binding string (like items
in data-bind="if: items().length"
) are read when the valueAccessor()
function is called (or via allBindingsAccessor
- allBindingsAccessor.get("if")
in this case). I think that it is useful to think of a binding’s update
function just like a normal computed observable where access to any dependencies will trigger the binding to run again.
Note: Prior to KO 3.0, all bindings on a single element were wrapped inside of a single computed. The parsing and evaluation of the binding string was also included in this computed. So, calling valueAccessor()
would give the result of the expression rather than actually run the code. Dependency detection worked the same way, all bindings on an element were triggered together and it was not possible to isolate dependencies made in the binding string. See this post for more details.
Something new in KO 3.2 - ko.pureComputed
There is an interesting feature coming in KO 3.2 related to computed dependency tracking. Michael Best implemented an option that allows a computed to not maintain any dependencies when it has no subscribers to it. This is not appropriate for all cases, but in situations where the computed returns a calculated value with no side-effects, if there is nothing depending on that calculated value, then the computed can go to “sleep” and not maintain subscriptions or be re-evaluated when any of the dependencies change. This will be useful for efficiency as well as potentially preventing memory leaks for a computed that was not disposed, but could be garbage collected if it was not subscribed to something observable that still exists. The option is called pure
and a ko.pureComputed
is provided as a shortcut.