The magic of KnockoutJS unveiled
We saw that all the magic of KnockoutJS starts with the call to:
ko.applyBindings(myViewModel);
This function gets two parameters: a View Model and a DOM element. You can skip the second parameter and it will default to the document.body
.
First of all, it takes the View Model, and makes a ko.bindingContext
from the View Model.
BindingContext tracks all the following information:
$parent
: This is the View Model of the parent context; for example, every binding inside a foreach binding will have the foreach view model as $parent$parents
: This refers to an array with all the parents context; empty for the root View Model. You can use an indexer to traverse the hierarchy (for deep-nesting); for instance, $parents[1] will get you the 2nd ancestor and so on$root
: This is the View Model of the highest parent; itself for the root view model.$rawData
: This is the original View Model, before unwrapping (to understand "unwrapping" better, imagine that you have a property,x = ko.observable(12)
, and you executex()
; you are unwrapping the observable to get the value12
)$data
: This refers to the unwrapped View Model.
Then, it starts to apply the bindings to the node:
- It stores the
bindingContext
inside the node data (but only if the current context is different from the context inside the parent node) - It checks if the current node contains the data-bind attribute, and applies the binding to each of them
- For each binding, it executes the init function inside a call to ko.dependencyDetection.ignore, and then the update function inside a call to ko.dependentObservable; in this way, the update function of each binding handler works as a computed observable (more about computed observables a little later)
- It executes these steps recursively for each descendant
Note
Binding to the same node more than once is not permitted; when you call
ko.applyBindings
it checks if the node is already bound and it will throw an exception.When you think you need to apply the binding again (maybe you changed the DOM structure without KnockoutJS) to the same node, the best idea is to rethink why you should do it; often you will see you can use the with binding handler to solve this problem in a KnockoutJS way.
Or, if you are absolutely sure this is the best solution, you can use
ko.cleanNode
to reset the element to its previously unbound state.
The change of the bindingContext
is done inside a few binding handlers (with
, foreach
, and so on) because they create a child bindingContext
; you can do the same inside your custom binding handler's init
function (for more information visit this URL: http://knockoutjs.com/documentation/custom-bindings-controlling-descendant-bindings.html).
Note
Before looking at a practical example, let's understand what a computed observable is.
ko.computed
is the third kind of Observable KnockoutJS supports; it's defined by a function, and each time it runs it registers itself as subscriber of any Observable found during the evaluation.
This is the same method KnockoutJS uses for the binding handler you find in the View.
In a few words, a computed observable is an observer of another observer; the easiest example is the full name computed observable, defined as the concatenation of the observable, name, and the observable, last name:
var firstName = ko.observable("Bob"), lastName = ko.observable("Smith"); var fullName = ko.computed(function() { return firstName() + " " + lastName(); });
The property fullName
here gets evaluated each time one of its internal observables changes.
Let's understand step by step what happens when you execute ko.bindingHandler(viewModel)
in the current document.
We start with the following DOM structure:
As the first step, it takes the document.body
node to work on, as you can see in the following picture:
It creates and adds to the data of the node, body
, a new BindingContext
like this:
ko.bindingContext: { $root: obj, $rawData: obj, $parents: [], $data: obj }
Here obj
is the parameter, viewModel
.
Then it walks inside the descendants searching for the data-bind
attribute; the next node to work on is the following one:
Here it finds a foreach
binding, so it executes the binding handler; the init
function returns
controlsDescendantBindings, so it stops descending.
The function init
of foreach
saves the descendants and clears the DOM structure, so now we have this structure:
After this step it ends, because all the descendants of document.body
are bound to our view model.
When the code updates viewModel.jewels
with the content of the category list, the flow continues.