This is the first of a few posts about how jsBind works. I’m sure that knowing how jsBind works under the hood will make it much easier to work with and knowing how an API library works and what it does internally makes it much easier to get the most out of it.
jsBind uses a few different techniques to achieve its end. It is part language parser, part interpreter, part event manager and part API library. In part one I will start by describing how jsBind parses the binding expressions into an expression tree and I will continue in future posts to describe how expressions are evaluated and how they are re-evaluated when the underlying data is changed. First though we need to cover some background about how observable objects work so we can see how these interact with expression tree later.
Observable objects are based on the
gang of four design pattern called the
Observer Pattern (if you have not seen this before go take a quick look, its well worth a read as these design patterns are useful in many situations). An observable object is an object that tells one or more other objects when something happens to it. In our case we tell other objects by invoking callbacks whenever data held in our observable object changes.
For jsBind to recognize an object as being observable it must implement two methods. These are:
addObserver
removeObserver
The functions are quite straightforward, an observable object contains an internal list of methods to call when data changes and these methods manipulate that list. The first adds a method to the internal list and the second removes a method. It is up to the implementation of the observable object to decide how the list is maintained. jsBind objects just use a straightforward array with a couple of lines of extra code to lazily allocate the array when it is first needed and to delete the array as soon as its empty.
public addObserver(delegate: ChangeDelegate): void {
// Null/undefined delegates are ignored.
if (delegate == null) {
return;
}
// Allocate storage if its not been needed until now.
if (this._changeDelegates == null) {
this._changeDelegates = [];
}
this._changeDelegates.push(delegate);
}
public removeObserver(delegate: ChangeDelegate): void {
// Ignore if the delegate is null/undefined
var delegates = this._changeDelegates;
if (delegates == null) {
return;
}
// Remove the delegate if we can find it.
var index = delegates.indexOf(delegate);
if (index > -1) {
delegates.splice(index, 1);
// Delete the empty list entirely
if (delegates.length == 0) {
delete this._changeDelegates;
}
}
}
When the observable object wants to notify its observers it iterates through its internal list of methods and invokes each method with the data. The handling method is expected have one parameter that takes the name of the member that has changed. jsBind observable objects implement this in the
notify method.
jsBind supplies several objects that implement this Observer design pattern so most of the time you do not have to roll your own unless you really want to. These objects are:
Observable
ObservableValue
ObservableString
ObservableNumber
ObservableBool
ObservableCollection
The first one of these is a base class that provides the mechanism to add, remove, and notify subscribers. The next four use this base class for storing a single value of different types and to raise a notification every time the value is changed. The
ObservableString,
Number and
Bool objects are all type specific versions of the
ObservableValue object and are really of most use for TypeScript projects where type checking is provided.
The
ObservableCollection object is a bit more of a special case of the Observer pattern that allows for several optimizations used by jsBind to make the processing of anything that is based on a list of items much more efficient. Collections are mostly used with ForEach bindings to generate a list of UI elements. We want to avoid re-generating the entire list in the UI when just one element of the collection changes or is added or removed. To achieve this we extend the Observer pattern so that we can pass more information to the observer to indicate which items were changed and how they changed. This then allows the ForEach binding to work more efficiently by only changing the minimum amount of the UI to properly reflect the changes in the underlying data.
ObservableCollection uses the same
addObserver and
removeObserver methods as before except that this time they are called
addCollectionObserver and
removeCollectionObserver and they expect to be give a more complicated notification method to call:
(inserted: any[], insertedIndex: number, updated: any[], updatedIndex: number, deleted: any[], deletedIndex: number): void;
This method expects to be called with three lists and three numbers in pairs. These provide a list of items that have been added and the index at which the adding started, a list of the items that have been replaced (updated) and the index at which this started, and a final list of items that have been deleted and the index at which this started. The indexes are all based on the position in the array before any changes were made.
Of course not all operations on a collection would cause items to be added, updated, and deleted all at the same time. To handle this situation the unused arguments are passed an empty array and an index of -1.
That covers things as far as all the background on observable objects goes. It is worth noting that although jsBind provides these objects for you to use in your model code there is nothing to stop you from implementing your own observable objects directly within your model. If you have a model with lots of members that you need to bind you can either wrap each value in an
ObservableValue or you can directly implement the observer pattern and notify the name of the member that changes. You can use the
Observable object and derive from it in your own model or for TypeScript programs you can also implement the
IObservable and
IObservableCollection interfaces. The
Clock example is a good demonstration of this. It extends the
Observable base object and notifies the same set of observers whenever the hours, minutes, or seconds members change.
That's all for now. In the next post we will look at the expression parser. In the meantime you can find all the source code for jsBind on
GitHub and more information on the
jsBind web site.