Recently, I have been blogging mainly about Web API, but many people have been asking me about Knockout.js. And rightfully so, because it is one of the most robust Javascript client side technology right now.
I blogged about KO a couple of times here before, but I thought it might be a nice change to do it in a new format. Instead of a one big, end-to-end solution, let’s do a set of “pro tips”, small pieces of real-life advices for your Knockout solutions.
This article assumes a working knowledge of Knockout.
Tip #1. Performance improvement for ObservableArrays - work with underlying arrays π
It is easy to suffer from performance problems when manipulating large or rich (containing complex objects) observable arrays. Any time you perform any operation on such array, all the subscribers get notified and very often an avalanche of computations and UI updates is the consequence. In the development process, such a chain of events is normally not a visible bottleneck, until you get to a a rich, real-life user interface, where suddenly lags are causing problems in your application.
Imagine you are inserting 100 items into an observable array. More often than not, you don’t need each subscriber to recalculate it’s dependencies 100 items, and UI to be reacting 100 times. Instead, once should just fine.
To do this, you can always modify the underlying array instead of the observableArray directly, since observableArray concept is just a function wrapper around the traditional JS array. After you are done with the array manipulation, you can then notify all the subscribers that the array has changed its state. See the simple example:
function homeViewModel() {
var self = this;
self.teams = ko.observableArray([]);
}
$.getJSON("data/NCAAteams.js",function(response) {
var array = vm.teams();
ko.utils.arrayPushAll(array, response.sports[0].leagues[0].teams);
vm.teams.valueHasMutated();
});
In this example. we are using KO extensions to push all values received via XHR call into the underlying array, and then notifying the subscribers using valueHasMutated().
This is an example copied from an actuall application. The JSON here contains all NCAA Divison 1 basketball teams (346 of them), we don’t want to burden our UI and the browser to recalculate everything 346 times, which would be the case if we just looped through the results and added each item one by one.
Tip #2. If possible, throttle your computed observables π
This is not really speicifc for observableArrays, but all observables, but it is usually most evident with computed observables which depend on observableArrays and return a different array. Again, consider the following example, which allows users to filter the teams based on the text entered into a textbox (quite a common scenario):
function homeViewModel() {
var self = this;
self.teams = ko.observableArray([]);
self.filterText = ko.observable("");
self.filteredTeams = ko.computed(function() {
if (self.filterText().length > 0) {
var teamsArray = self.teams();
return ko.utils.arrayFilter(teamsArray, function(team) {
return ko.utils.stringStartsWith(team.location.toLowerCase(), self.filterText())
});
}
});
}
<input type="text" data-bind="value: filterText, valueUpdate:'afterkeydown'" />
<ul data-bind="foreach: filteredTeams">
<li>
<h3>
<span data-bind="text: location"></span> <span data-bind="text: name"></span>
</h3>
</li>
</ul>
We have a filteredTeams computed observable, which is dependant on the main teams observableArray and on the filterText observable.
Any user change to the filter text in the textbox will trigger a considerable amount of calculation to be happening (remember from previous example, our teams observable array has 346 items). As the user types new characters, the whole process get repeated. Note that you might have additional subscribers, so the amount of calculations could be even greater.
In such cases, especially if you are willing to support less computation efficient browsers such as older versions of IE or mobile browsers (they will not have as much CPU at their disposal as computer browser) you ushould really consider using Knockout’s throttle extender. Applied to a computed observable, it defers the execution of the dependencies by n milliseconds.
So in our example if the user types in a team name, he won’t really get any filtering until he stops writing for the given amount of milliseconds - in this case below, 750. This way we could save redundant CPU cycles to i.e. filter the array by starting letter “A” and so on.
self.filteredTeams = ko.computed(function() {
if (self.filterText().length > 0) {
var teamsArray = self.teams();
return ko.utils.arrayFilter(teamsArray, function(team) {
return ko.utils.stringStartsWith(team.location.toLowerCase(), self.filterText())
});
}
}).extend({ throttle: 750 });
Tip #3. Array manipulations made easy π
There are a lot of out-of-the-box functions that make working with observable arrays (and arrays in general) very easy. Those are located in the ko.utils namespace, so a lot of people are unaware of them, as they won’t come up with intellisense after adding a “dot” after your observableArray.
The functions in the ko.utils namespace include, among many:
- looping helpers
- filtering helpers
- searching helpers - possibility to match first result or get unique values
- array comparison utilities
You may have noticed that in the previous two examples I actually used ko.utils.arrayPushAll, ko.utils.arrayFilter and ko.utils.stringStartsWith. Another very useful one is ko.utils.arrayMap as it allows you to project your observableArray (or, for that matter, regular array as well) onto some other array and use that. This is especially helpful for computed observables.
I will not go into deep details about ko.util as there are already great resources about that. RP Niemeyer, the Knockout guru, has a terric article about that so I really recommend you check it out.
Tip #4. ObservableArrays are extensible π
You are not confined to using only the array manipulation functions that knockout provides out of the box, you can aeasily add your own.
Let’s say you want to add custom sorting to your observableArray. You are holding custom objects in that array and would like to sort them in the array based on one of their properties.
In our example the objects to be sorted look like this:
{
"id": 96,
"location": "Kentucky",
"name": "Wildcats",
"abbreviation": "UK",
}
Let’s extend observableArrays to provide this custom sorting:
ko.observableArray.fn.sortByProperty = function(prop) {
this.sort(function(obj1, obj2) {
if (obj1[prop] == obj2[prop])
return 0;
else if (obj1[prop] < obj2[prop]) return -1 ; else return 1; }); }
``` Now you can invoke the sorting by simply calling (where vm.teams is your observable array):
```javascript
vm.teams.sortByProperty("name"); vm.teams.sortByProperty("location"); vm.teams.sortByProperty("abbreviation"); vm.teams.sortByProperty("id");
Summary π
Working with arrays in knockout is a very broad subject, however I hope these few tips could help you get on the right track. There are always many ways to achieve things in javascript, but choosing the right tools in knockout.js can make your life much easier.
Additionally, when working with large data sets, complex collections of objects and rich user intefaces it is very easy to run into performance problems with knockout.js. Thankfully there are ways to avoid these, and smart array manipulation, rather than brute force can be one of the best performance boosts for your application.