How bindings are processed
Update: In Knockout 3.0, bindings are now fired independently on a single element, so this is no longer an issue for KO 3.0+.
When using multiple bindings on a single element, it is important to understand how Knockout triggers updates to bindings to avoid potential performance issues. For example, a common binding might look like:
1
|
|
How are the bindings on this element processed? When Knockout determines that an element has bindings, a computed observable is created to aid in tracking dependencies. Inside the context of this computed observable, Knockout parses the data-bind
attribute’s value to determine which bindings to run and the arguments to pass. As the init
and update
functions for each binding are executed, the computed observable takes care of accumulating dependencies on any observables that have their value accessed.
Here is a simplified flow chart of the binding execution:
There are a couple of important points to understand here:
The
init
function for each binding is only executed once. However, currently this does happen inside the computed observable that was created to track this element’s bindings. This means that you can trigger the binding to run again (only it’supdate
function) based on a dependency created during initialization. Since, theinit
function won’t run again, this dependency will likely be dropped on the second execution (unless it was also accessed in theupdate
function).There is currently only one computed observable used to track all of an element’s bindings. This means that the
update
function for all of an element’s bindings will run again when any of the dependencies are updated.
This is definitely something to consider when writing custom bindings where your update
function does a significant amount of work. Whenever bindings are triggered for the element, your update
function will run, even if none of its observables were triggered. If possible, it is a good idea to check if you need to do work, before actually executing your update
logic.
Common problems with the default bindings
1- A common scenario where this can cause an issue is when using the template
binding in conjunction with other bindings. For example, you may attach a visible
binding along with a template
binding like:
1
|
|
In this case, if showPlaylist
is frequently updated, it will cause the template
binding to re-render the template again. In some cases, this may not cause a concern (it would just behave like the if
binding). However, in a scenario where the template has significant markup and the visible
binding’s condition is frequently triggered this can cause an unnecessary performance hit. Note that when using the foreach
option or binding, logic is executed to determine if any items were added or removed, so it will cause less of a performance hit in that case.
2- Another place where this can come into play with the default bindings is when using the options
and value
bindings together. The update
function of the options
binding rebuilds the list of option tags for the select element. Whenever the value
is updated, it will trigger all of the bindings on the element to execute. So, instead of just setting the appropriate value, it will rebuild all of the options and then set the value. If you have a situation where you have a large number of options, then this can cause a performance issue.
Ways to address this concern
1- In some cases, it makes sense to put bindings on separate elements or on a container element. For example, you may be able to move a frequently triggered visible
binding to a parent element rather
have it coupled with other bindings like the template
binding.
1 2 3 |
|
Here is a sample showing a visible
and template
binding on the same and different elements:
2- In the case of the options
and value
bindings, you can choose to build your option elements separately. It would be nice to just use a containerless foreach
statement inside of a select
element, but Internet Explorer will remove comments that it finds between select
and option
elements. An alternative would be to use Michael Best’s repeat binding on the option
element like:
1 2 3 |
|
3- A more advanced way to handle these issues, is to create your own computed observable in the init
function to handle updates yourself. Any observables accessed in your computed observable, will not be a dependency of the overall computed observable used to track all of the element’s bindings.
This is a technique that I tend to use by default in any bindings that I write. It is also useful when you want a single binding to accept multiple observable options and you want to respond separately to each one changing (as opposed to using the update
function to repond when any observable changes). You can even wrap the existing bindings in this way to create an isolatedOptions
binding.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
A few notes on this technique:
- The idea is that we want to trap our dependencies in our own isolated computed observable.
- We call the update function of the
options
binding using.apply
with the original arguments that were passed ot the binding. - The
disposeWhenNodeIsRemoved
option ensures that this computed observable will be destroyed if Knockout removes our element, like in a templating scenario. - There is one minor issue with using this technique currently: observables that are accessed in the actual binding string are included in the overall computed observable during parsing rather than when you create you call
valueAccessor
. This means that if your binding contains an expression where you access the observable’s value (text: 'Hello ' + name()
) , then it will be tracked in the overall computed observable. This is likely to change in the near future.
Here is a sample that shows using #2 and #3 with options
and value
bindings: http://jsfiddle.net/rniemeyer/QjVNX/
Future
There has been some thought and work put into running each binding in the context of its own computed observable. Michael Best has this working properly in his Knockout fork. As these changes can be considered breaking, they will likely be carefully implemented over time using an opt-in approach, perhaps until a major version allows us to make some potentially breaking changes. For now, it is wise to keep in mind how your bindings are triggered and how that affects other bindings on the same element.