calculations with knockout observables blows up calculation time
-
21-12-2019 - |
Question
I have noticed a problem when I do some heavy calculations with ko observables.
An example of the problem you will find at http://jsfiddle.net/dundanox/AyU8y/1/
To keep it short I have an input field and an observable "val"
<input data-bind="value: val">
Now, there are two ways to change the value of the observable.
1. Typing a (new) value in the input field manually
2. Assigning a (new) value by script, e.g. ViewModel.val(3.14)
After setting a value I do some heavy calculations, e.g.
var val = ViewModel.val(); // get current value, e.g. 3.14
for(var sum=0, ii=0; ii > imax; ii++)
sum += val
If I set a value by script (second method), everything is fine. But if I set a value manually (first method), the calculation time blows up multiple times!
I think it is s strange behavior und should not be. But I can't find the problem. Is it a problem within knockoutJS?
To clarify it, with the following code everything is fine.
var val = 3.14;
for(var sum=0, ii=0; ii > imax; ii++)
sum += val
My understanding of the line
var val = ViewModel.val(); // get current value, e.g. 3.14
should be the same as if i write
var val = 3.14;
It seems it depends on how I set the value of the observable. Why it is so? And how can I fix it?
Solution
When you type it it's a string, string operation is slower than number
use parseFloat
The result is also wrong, concating strings and numbers are not the same thing
OTHER TIPS
Anders is right! the operation is more expensive because javascript needs to make an implicit typecasting on each iteration. Maybe you had heard about the ===
operator, which is recomended because it compares both type and value, different from ==
operator, which compares only value but makes an implicit typecasting on the values you are comparing.
Hope it helps!
The root cause of your problem is that when knockout is notified of the input's value change it reads the new value from the input field and writes that back to the observable.
The input's value is a string so that is what KO puts into the observable.
If you expect it to be a number then you will need to consistently force the value into a number. The best way (IMHO) of doing this is via the fn extension point.
ko.observable.fn['asNumber'] = function (defaultValue) {
var target = this;
var interceptor = ko.computed({
read: target,
write: function (value) {
var parsed = parseFloat(value);
var manualNotifyFlag = false;
if (isNaN(parsed)) {
parsed = defaultValue;
manualNotifyFlag = (target() === parsed);
}
if (!manualNotifyFlag) {
target(parsed);
} else {
target.valueHasMutated();
}
}
});
interceptor(target()); // Ensure target is properly initialised.
return interceptor;
}
Create our observable use the following
val: ko.observable(3.14).asNumber(0)
Now when the observable's value is set, regardless of whether you do it manually using a number type, or knockout via the elements change event using a string, the observable's value will be forced into a numeric.
This saves you from putting parseFloats all over your codebase.
I have updated the fiddle to show this.
Also, the parseFloat statement in the extension can easily be retrofitted to support any globalization engine, once again only having to do this in one place in your codebase
// Using the jQuery Globalize library from http://github.com/jquery/globalize
var parsed = (typeof (value) === "string" ?
Globalize.parseFloat(value) :
parseFloat(value));