Knockout is amazing. It is fast and intuitive. I use the subscribe function a lot, but I found myself lacking a general subscribe that allows me to track the changes of an entire ViewModel
, so I created one myself that even supports unsubscribe and throttling.
The basic idea
So what are we going to do? Given a ViewModel
, we'll:
- Find all observable properties
- Subscribe to those properties
- Inspect the value of each observable property and do the same: find, subscribe and inspect.
We'll extend on the ko
variable, so we'll get the following new features:
- To subscribe a model:
ko.subscribe(vm, fn, 100);
- To unsubscribe a model:
ko.unsubscribe(vm, fn);
So let's dive in!
First things first: LoDash
Lo-Dash is a low-level utility library that implements common operations. It was created as a fork of the Underscore project. Unlike most libraries, Lo-Dash eschews almost all native iteration methods in favor of simplified loops, resulting in tight, lean code (read more). It makes code faster and more readable. I use it for array loops and to implement the throttling feature.
Tha code
A while back I wrote a blog on Automatic Knockout model persistence with Amplify. I'll be using the same method for looping through the properties of a model.
(function () {
//stores the subscriptions
var subscriptions = [];
//maintains a unique identifier
var id = 0;
ko.subscribe = function (vm, fnOnChange, throttle) {
var myId = ++id;
//store subscription - add throttle
subscriptions.push({
id: myId,
change: _.throttle(function () {
fnOnChange(vm);
}, throttle)
});
//subscripe model with id.
_subscribe(vm, myId)
};
ko.unsubscribe = function (vm, fnOnChange) {
_.remove(subscriptions, function (item) {
return item.vm == vm && item.change == fnOnChange;
});
};
function _subscribe(vm, id) {
if (_.isArray(vm)) {
//loop through array values and subscribe to each item
for (var i = 0; i < vm.length; i++) {
_subscribe(vm[i], id);
}
}
else {
//prevent double subscriptions by checking
//a 'magic' property:
var subscriberId = '_ko_subscr_' + id;
if (_.isUndefined(vm[subscriberId])) {
vm[subscriberId] = true;
//subscribe to each observable
for (var n in vm) {
var observable = vm[n];
if (ko.isObservable(observable) && !ko.isComputed(observable)) {
observable.subscribe(function (newValue) {
//subscribe the new observable value
_subscribe(newValue, id);
//fire event, because something just changed
fire(id);
});
//subscribe to current value stored by observable
var currentValue = observable();
_subscribe(currentValue, id);
}
}
}
}
}
function fire(id) {
_.forEach(subscriptions, function (item) {
if (item.id == id) {
item.change();
}
});
}
})();
ko.isComputed = function (instance) {
if ((instance === null) || (instance === undefined) || (instance.__ko_proto__ === undefined)) {
return false;
}
if (instance.__ko_proto__ === ko.dependentObservable) {
return true;
}
// Walk the prototype chain
return ko.isComputed(instance.__ko_proto__);
};
So basically that's it.
Demo
I've created a Random Greeting demo in JsFiddle, so you can see this code in action.