Knockout subscribe is blocking the page

故事扮演 提交于 2019-12-24 17:09:11

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!