Barbarian Meets Knockout: Knockout.js Computed Observables
The “barbarian meets” series are a collection of articles that intend to introduce and explain useful libraries, frameworks, tools and technologies in simple and straightforward terms. These new series will focus on Knockout.js, the popular JavaScript MVVM library
Hi everyone! Time to continue learning knockout.js! This time I will write about computed observables, a special kind of Knockout.js Observables that allow you to calculate values in the client from other properties of your view model with one nifty special feature: A computed observable will listen for changes in the observables it depends of and recalculate its value when these observables change.
Barbarian Meets Knockout
- Introduction to Knockout.js
- Knockout.js Observables
- Knockout.js Computed Observables
- Introduction to Knockout.js Observable Arrays
- Knockout.js Observable Arrays in Knockout 3.0
- Knockout.js Bindings
- Knockout.js Templating Engine
- Extending Knockout.js with Custom Bindings
- Inside the Source Code
- Scaling Knockout.js
- Knockout.js and SPAs (Single Page Applications)
- Persisting Data When Using Knockout.js in the Front End
- Using Knockout in an unobstrusive fashion
- The Bonus Chapters
Computed Observables
Occasionally, and particularly when building SPAs - Single Page Applications - with a high degree of interactivity you will get to the point where you are going to need to calculate values from other properties of your view model and bind them to your views. Be it because the data that comes from the backend is not in the format that you want, or due to the fact that data is missing/normalized in the domain model, you will need to extend your view model with computed observables.
Computed observables, in their most common incarnation, are read-only calculated properties. They are created by using the ko.computed
function and passing in a function that expresses how to calculate the desired value. Any observable used to calculate this value, will be recorded as a dependency of the computed, and, if changed, will trigger the recalculation of the value of the computed. The example below illustrates how to create a computed observable:
// Model
var player = {
name: "Kull",
title: "The Conqueror"
// etc...
};
// ViewModel
var PlayerViewModel = function(){
var self = this;
self.name = ko.observable(player.name);
self.title = ko.observable(player.title);
self.fullName = ko.computed(function(){
return self.name() + ', ' + self.title();
});
// etc...
};
In this example. the computed observable fullname
will be recalculated whenever the name
and/or title
observables change. Additionally, computed observables can also depend on other computed observables and they are bound to DOM elements just as any other observable.
Gotcha’s and Good Things to Know About Computed Observables
There are two very important things to know about computed observables that may trip you in some scenarios:
-
The value of a computed observable is updated only if any of the observables that were evaluated during the last execution changes. That is, if an observable is not evaluated when calculating a computed observable, then it changing will not trigger an update in the computed. Additionally, if the computed depends on another vanilla property, i.e. one that is not an observable, changes in this property will not trigger an update. (See example below)
-
By default, their value is calculated as soon as the computed is created. This means that we may run into issues if the computed is calculated from properties that are not yet loaded when the computed is created.
var PlayerViewModel = function(){
// ...
// change trancking system
self.hasChanged = ko.observable(false);
self.isSaving = ko.observable(false);
self.canSave = ko.computed(function(){
return self.hasChanged() && !self.isSaving();
});
// if hasChanged is falsy, then isSaving is not evaluated and thus
// won't cause the canSave computing to be updated
self.name.subscribe(function(newValue){
self.hasChanged(true)
});
self.saveChanges = function(){
self.isSaving(true);
app.playerRepository.save(self).done(function(){
self.hasChanged(false);
self.isSaving(false);
console.log('saved!');
});
};
};
An additional good thing to know is that you can use the peek
function within your computed to avoid creating a dependency between your computed and another observable. This can come in handy when you don’t want to trigger unnecessary recalculations of the computed. The knockout.js documentation for computed observables has a particularly good example of this:
ko.computed(function() {
var params = {
page: this.pageIndex(),
selected: this.selectedItem.peek() // this doesn't trigger updates in the computed
};
$.getJSON('/Some/Json/Service', params, this.currentPageData);
}, this);
Notice how, only when changing the page the ajax call is executed. The selectedItem observable does not trigger updates in the computed because we make use of the peek
method.
You can find some additional examples of computed observables in the knockout.js documentation for computed observables. Additionally, John Papa makes an interesting use of computed observables by creating a custom dump
data binding (more on custom data bindings in future articles) that uses a computed observable to dump whole view models into your DOM for debugging purposes. Refer to the pen&paper RPG demo below or this gist in GitHub for a simplified version of this custom binding.
Check out this Pen!
Deferring Evaluation of Computed Observables
To solve the issue I mentioned in the previous section, that one caused by the fact that a computed observable will calculate its value on creation and crash and burn if some of its dependencies haven’t been loaded yet, knockout.js offers deferred evaluation. We can postpone the evaluation of a computed observable until the first time it is accessed by using the deferEvaluation
option as illustrated in the example below:
var PlayerViewModel = function(){
var self = this;
self.name = ko.observable(player.name);
self.title = ko.observable(player.title);
self.fullName = ko.computed({
read: function(){
return self.name() + ', ' + self.title();
},
deferEvaluation: true
});
// ...
};
Note how we have used a slightly different notation than before and, instead of passing a function, we use an object {read: function(){...}, deferEvaluation: true }
.
Disposing Computed Observables
In order to avoid the famously acclaimed memory leaks Single Page Applications are so commonly associated with, it is a recommended practice that we dispose our computed observables. Note that disposing a computed observable will not destroy the object, it will just remove all its subscriptions so that the computed won’t ever be updated again from other observables (i.e. its read
method will not be triggered).
You can dispose a computed observable either explicitly by using the dispose
function:
var PlayerViewModel = function(){
var self = this;
// ...
self.fullName = ko.computed(...);
// ...
self.dispose = function (){
self.fullName.dispose();
};
};
or by using the disposeWhen
option with a function that will be evaluated every time the computed is updated and which will dispose the computed if it returns a truthy value:
var PlayerViewModel = function(){
var self = this;
// ...
self.disposeAll = false;
// You can test this in the sample by enabling the dispose all checkbox
// and removing items from the backpack
// You'll see how the (overstrained) status is never removed
// because this computed is disposed
self.status = ko.computed({
read: function(){
var totalWeight = parseInt(self.inventoryWeight());
if (!isNaN(totalWeight) && totalWeight > 20)
return "(overstrained)";
return "";
},
disposeWhen: function(){
return self.disposeAll;
}});
};
Finally, we can resort to a third option disposeWhenNodeIsRemoved
, which is usually used in conjunction with custom bindings. That’s why I will return to it in future articles.
Setting the Context for a Computed Observable Explicitely
In those corner cases where you want to make use of dynamic binding and grow your view model in a more dynamic fashion, you can take advantage of yet another extended version of creating knockout observables and pass the context (this
) explicitely:
var PlayerViewModel = function(name, characterClass, race){
var self = this;
self.name = ko.observable(name);
self.charactedClass = ko.observable(characterClass);
self.race = ko.observable(race);
};
var vm = new PlayerViewModel("Drizzt Do'Urden", 'Ranger', 'Drow');
// later
vm.magicResistance = ko.computed(function(){
gameEngine.Magic.calculateMagicResistance(this.race); // this being the view model
}, vm);
Writeable computed observables
Knockout computed observables are not read-only properties really, as you may have guessed already from the object notation we saw earlier. You can use the same notation to define a function to use when writing an observable.
// Model
var player = {
name: "Kull",
title: "The Conqueror"
// etc...
};
// ViewModel
var PlayerViewModel = function(){
var self = this;
self.name = ko.observable(player.name);
self.title = ko.observable(player.title);
self.fullName = ko.computed({
read: function(){
return self.name() + ', ' + self.title();},
write: function(value){
var nameAndTitle = value.split(", ");
self.name(nameAndTitle[0]);
self.title(nameAndTitle[1]);
}});
};
You can find this simple example in jsFiddle. For more examples of writeable observables, take a look at the knockout.js documentation.
Manually Subscribing to a Computed Observable
As it happens with vanilla observables, you can subscribe to changes in a computed and perform additional tasks.
A sneak peek inside the Source Code: Computed Observables
So! Finally! It’s time to look to some real Knockout.js source code. You can find the source code for computed observables under src/subscribables/dependentObservable.js
. Note that I have appended Jaime to my comments to difference them from those of the open source artists that maintain knockout.
ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, options) {
var _latestValue,
_hasBeenEvaluated = false,
_isBeingEvaluated = false,
_suppressDisposalUntilDisposeWhenReturnsFalse = false,
readFunction = evaluatorFunctionOrOptions;
// Jaime: I have removed some code to focus on the most important stuff
function addSubscriptionToDependency(subscribable) {
// Jaime: Here we subscribe to changes in the observables that have been
// evaluated within the computed function
// See how we execute evaluatePossiblyAsync when this occurs (4)
_subscriptionsToDependencies.push(subscribable.subscribe(evaluatePossiblyAsync));
}
function evaluatePossiblyAsync() {
var throttleEvaluationTimeout = dependentObservable['throttleEvaluation'];
if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) {
clearTimeout(evaluationTimeoutInstance);
evaluationTimeoutInstance = setTimeout(evaluateImmediate, throttleEvaluationTimeout);
} else
evaluateImmediate();
}
// Jaime: Here is were the computed function is evaluated
function evaluateImmediate() {
if (_isBeingEvaluated) {
return;
}
if (disposeWhen && disposeWhen()) {
// See comment below about _suppressDisposalUntilDisposeWhenReturnsFalse
if (!_suppressDisposalUntilDisposeWhenReturnsFalse) {
dispose();
_hasBeenEvaluated = true;
return;
}
} else {
// It just did return false, so we can stop suppressing now
_suppressDisposalUntilDisposeWhenReturnsFalse = false;
}
_isBeingEvaluated = true;
try {
// Initially, we assume that none of the subscriptions
// are still being used (i.e., all are candidates for disposal).
// Then, during evaluation, we cross off any that are in fact
// still being used.
var disposalCandidates = ko.utils.arrayMap(_subscriptionsToDependencies, function(item) {return item.target;});
// Jaime: Here we activate dependency tracking (1)
ko.dependencyDetection.begin(function(subscribable) {
var inOld;
if ((inOld = ko.utils.arrayIndexOf(disposalCandidates, subscribable)) >= 0)
disposalCandidates[inOld] = undefined; // Don't want to dispose this subscription, as it's still being used
else
addSubscriptionToDependency(subscribable); // (2) Brand new subscription - add it
});
// Jaime: Here we calculate the new value while detecting dependencies
var newValue = evaluatorFunctionTarget ? readFunction.call(evaluatorFunctionTarget) : readFunction();
// (3) For each subscription no longer being used, remove it from the active subscriptions list and dispose it
for (var i = disposalCandidates.length - 1; i >= 0; i--) {
if (disposalCandidates[i])
_subscriptionsToDependencies.splice(i, 1)[0].dispose();
}
_hasBeenEvaluated = true;
// Jaime: Notify subscribers if the computed changed (5)
if (!dependentObservable['equalityComparer'] ||
!dependentObservable['equalityComparer'](_latestValue, newValue)) {
dependentObservable["notifySubscribers"](_latestValue, "beforeChange");
_latestValue = newValue;
if (DEBUG) dependentObservable._latestValue = _latestValue;
dependentObservable["notifySubscribers"](_latestValue);
}
} finally {
ko.dependencyDetection.end();
_isBeingEvaluated = false;
}
if (!_subscriptionsToDependencies.length)
dispose();
}
// Jaime: This is the object that will get assigned to the computed
// properties in our view model
function dependentObservable() {
if (arguments.length > 0) {
if (typeof writeFunction === "function") {
// Writing a value
writeFunction.apply(evaluatorFunctionTarget, arguments);
} else {
throw new Error("Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.");
}
return this; // Permits chained assignments
} else {
// Reading the value
if (!_hasBeenEvaluated)
evaluateImmediate();
ko.dependencyDetection.registerDependency(dependentObservable);
return _latestValue;
}
}
// By here, "options" is always non-null
var writeFunction = options["write"],
disposeWhenNodeIsRemoved = options["disposeWhenNodeIsRemoved"] || options.disposeWhenNodeIsRemoved || null,
disposeWhenOption = options["disposeWhen"] || options.disposeWhen,
disposeWhen = disposeWhenOption,
dispose = disposeAllSubscriptionsToDependencies,
_subscriptionsToDependencies = [],
evaluationTimeoutInstance = null;
// Jaime: here you see how, by default, a computed is evaluated when created
// Evaluate, unless deferEvaluation is true
if (options['deferEvaluation'] !== true)
evaluateImmediate();
return dependentObservable;
};
ko.exportSymbol('dependentObservable', ko.dependentObservable);
// Jaime: here is the computed alias we all know and love
ko.exportSymbol('computed', ko.dependentObservable);
So, in summary, and as you can attest from the source code, a computed observable:
- Records all dependencies when it calculates its value
- It subscribes to those dependencies
- It disposes subscriptions from previous runs that were not discovered during the latest run.
- It reevaluates its value whenever changes in its dependencies are detected
- It notifies its subscribers when it has changed
Take a look at the computed observable behaviors test suite and the computed observable DOM behaviors test suite for more interesting stuff.
Additional References
Conclusion
And that’s a great deal of what you need to know about computed observables. Hope you’ve learned something new. I will be coming back to computed observables when writing about templates, custom bindings and performance. Have a good one!
P.S. On a completely unrelated topic… Have you tried Sublime Text’s distraction free mode? If you haven’t, give it a try, it feels very… how to put it… distraction… free :)
P.S.2. Oh my god… I was adding links to this blog post to the previous blog posts of these series and I noticed that I wrote the first one in April this year. What happens with time!? :)
Written by Jaime González García , dad, husband, software engineer, ux designer, amateur pixel artist, tinkerer and master of the arcane arts. You can also find him on Twitter jabbering about random stuff.