问题
I have an observable array with a computed to get the currently selected item of a radio list.
self.KOVehicle.SelectedPolicy = ko.computed(function () {
return ko.utils.arrayFirst(self.KOVehicle.VehicleDetailsList(), function (veh) { return veh.PolicyId() == self.KOVehicle.SelectedPolicyId(); });
}, self);
I'm subscribing to changes on that which will update a dozen or so various bound items on the page.
self.KOVehicle.SelectedPolicy.subscribe(function (selectedPolicy) {
// show ajax loading spinner and do changes
});
Now it all works, except it seems to block the page while it's doing the changes. I don't see the ajax loading spinner unless I open the js debugger and step through it. Any ideas?
回答1:
I think I have seen the same problem. In my current KO application, the user can click edit on an item. I load a list of attributes associated with that item. This triggers both some ko.computed functions, and a large KO template with a lot of nested foreach statements. KO evaluates all this synchronously, which means it all has to complete before the JavaScript interpreter returns to the next line of my code. The DOM manipulation in my inefficient foreach loops took 2-3 seconds, which created the appearance that the page "blocks" for about 2-3 seconds, as you describe.
Here's the problem, very briefly, as a mix of HTML and JS:
<button data-bind="click: edit">Edit</button>
// This is my list of attributes. A lot of computeds and a big template depend on
// this observable.
var list = ko.observableArray();
edit: function(data, event) {
// Update the list
list(data.list)
// Now wait 2-3 seconds for anything to happen.
}
So to give the user some immediate feedback, I tried adding a loading spinner, loosely this:
<button data-bind="click: edit">Edit</button>
// This is my list of attributes. A lot of computeds and a big template depend on
// this observable.
var list = ko.observableArray();
edit: function(data, event) {
// Update the list
list(data.list)
$(".spinner").show() // Doesn't appear.
// Now wait 2-3 seconds for anything to happen. The spinner only then appears.
}
OK, so obviously I thought I could fix it by swapping the lines:
<button data-bind="click: edit">Edit</button>
// This is my list of attributes. A lot of computeds and a big template depend on
// this observable.
var list = ko.observableArray();
edit: function(data, event) {
$("#spinner").show() // Still doesn't appear
// Update the list
list(data.list) // Blocks page
// Now wait 2-3 seconds for anything to happen.
}
This still doesn't work - the spinner doesn't appear until after the render completes. Here's a jsfiddle demonstrating exactly this problem. You can see the effect of the page "block" because the button remains frozen in its active state until the list has rendered. And the spinner doesn;t appear until after KO completes, even though I try to make it appear first:
http://jsfiddle.net/6f6Rt/3/
My original answer suggested using a timeout as a hack to workaround this, and I still cannot find a better way. Here is a fiddle showing a timeout and also using a computed:
http://jsfiddle.net/222CG/2/
Here is the timeout hack:
// Show the spinner immediately...
$("#spinner").show();
// ... by using a timeout wrapped around the thing that causes the delay.
window.setTimeout(function() {
ko.applyBindings(vm)
}, 1)
In this fiddle, if you put a
console.log("computing")
inside the computed, you'll see it is computed 3001 times, once initially, and once for every item we push into vm.items(). While reading about improving the performance of my app, I came across throttle:
http://knockoutjs.com/documentation/throttle-extender.html
Here's a fiddle showing throttle on the computed; also with a console output; now you can see it is only called twice, which must be worth a marginal performance gain!
http://jsfiddle.net/M4CUq/1/
With respect to my code, it is deployed to a tablet and it is even slower there, especially on old Androids with slow CPUs, so I concluded that I really should optimise it. I had nested for-each loops, both working on list, so essentially I had an O(n^2) problem. I rewrote the list data structure so that I could achieve my goal with a single foreach loop. I got the 2-3second delay down to less than half a second.
So in conclusion:
I doubt you're doing anything fundamentally wrong; but it is possible with Knockout to trigger a synchronous set of operations that can last for several seconds, especially if it includes DOM manipulation. This can be seen in the fiddles above, which use a simple template, but a big loop, to block the page for ~3 seconds,
The best thing you can do is to try to optimise your rendering code and throttle some computeds
and to help with UX, you can use the timeout hack to ensure a spinner is visible before you trigger the long KO operation
oh, and finally... how do you hide the spinner when everything is done? I settled on using the afterRender callback of a template binding, which you can do without a template itself, as such, so here is a final fiddle showing that in operation!
http://jsfiddle.net/A5tZU/
Slightly different approach 1: ongoing reading -
This fiddle shows a technique for using a timeout to push each item into the list one-by-one. By putting the timeout between the push operations, the DOM updates item-by-item. So the overall render time is still quite large, but the user gets immediate feedback:
http://jsfiddle.net/rosenfeld/7TwcV/1/
Slightly different approach 2: another question -
I've just answered another Q, where I try an innerHTML trick to greatly improve the performance of rendering a large list.
Binding around 5000 records using knockout
来源:https://stackoverflow.com/questions/24856219/knockout-subscribe-is-blocking-the-page