Custom knockout binding fires twice, unexpectedly

2019-09-20 09:29发布

问题:

I have written a custom binding handler to bind my viewmodel data to a highcharts chart. This really has 2 parts, one binds the initial config required for highcharts, the second binds the series to the chart.

here is the bindingHandler code

ko.bindingHandlers.highchart = {
    update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        var value = valueAccessor();
        var valueUnwrapped = ko.unwrap(value);
        console.log ('update',element.id,valueUnwrapped);        
        if(allBindings.get('series')){
            var series = allBindings.get('series');
            var seriesUnwrapped = ko.unwrap(series);
            if(!$.isArray(seriesUnwrapped)){
                seriesUnwrapped = [seriesUnwrapped];
            }
            console.log('seriesUnwrapped',element.id,seriesUnwrapped)
            valueUnwrapped.series = seriesUnwrapped;
        }
        $(element).highcharts(valueUnwrapped);        
    }
}

Now I have 2 tests set up for this, the first works as expected. It binds a chart with multiple series, and when I add to the observable array bound to the series it updates the chart just once. Look at this fiddle and watch the console as you click the "add" button. The output you'll get is

update container Object { chart={...}}
seriesUnwrapped container [Object { name="Scenario0", color="red", data=[9]}, Object { name="Scenario1", color="green", data=[9]}]

Indicating that we've been through the above code only once.

Now check my second fiddle: http://jsfiddle.net/8j6e5/9/ . This is slightly different as the highcharts initial config is a computed observable, as is the series. When you click the "add" button on this one you'll see the binding is executed twice:

update container2 Object { chart={...}, xAxis={...}, series=[1]}
seriesUnwrapped container2 [Object { name="Scenario2", color="blue", data=[2]}]
update container2 Object { chart={...}, xAxis={...}}
seriesUnwrapped container2 [Object { name="Scenario2", color="blue", data=[2]}]

I'm guessing that using allBindings.get('series') within my highcharts binding handler I set up a dependency to it, and when both bindings change its executing the highcharts binding twice. My question is, is there any way to stop this, or write this functionality any other way as to not have this happen?

回答1:

I'm not sure this will help you, as nemesv answers in the comments above seem to be very close to what you wanted to achieve, ie. stopping the double update.

However, I've spent a bit of time on it, so I'll show you what I came up with anyway and hope it helps.

Fiddle here

I didn't know about the peek() method that nemesv mentioned (great tip), so I looked into why it was updating twice based on the computeds etc.

I saw that your self.breakdownChart was accessing the currentScenario observable, and when I removed that as a test, the second update didn't occur.

So that got me thinking, why you needed that in there for the x axis setting.

So I added a new property to your scenario to return the current scenario name

 self.name='Scenario' + self.number;

And then for the base scenario, changed this to "Base Scenario" to ensure that title appears correctly for just that series.

To ensure the legend/axis is correct, I added a new property to the chart object called baseSeriesName

self.breakdownChart = ko.computed(function(){
    return {    
        baseSeriesTitle: baseScenario.name,

and that is set to the baseScenario's name.

Finally, to tie that all together in the BindingHandler, I update the xAxis in there:

       //set the xAxis titles, only add the second title if different from the base
        valueUnwrapped.xAxis={
            categories: [valueUnwrapped.baseSeriesTitle, valueUnwrapped.baseSeriesTitle!=seriesUnwrapped[0].name ? seriesUnwrapped[0].name:'']
        }   

It's a bit of refactoring, but it achieves your goal; hope it helps.

Oh, I also added a chartType observable to the view model, and used that in the chart definition (breakdownChart computed), to test the double update wouldn't happen if the chart refreshed on a different observable and that it still initialised correctly - so the fiddle shows the chartType updating, without a double update.



回答2:

You get two updates because Knockout updates computed observables immediately when their dependencies change, and your binding has two dependencies, each of which gets updated in turn.

One way to solve this is to use a technique to delay updates of the binding. An easy way to do so is to use the Deferred Updates plugin, as demonstrated here: http://jsfiddle.net/mbest/8j6e5/15/

Deferred Updates uses setTimeout to perform updates, which means that the update happens asynchronously. If you want it to be synchronous for a specific update, you can use ko.tasks.processImmediate:

ko.tasks.processImmediate(function() {
    self.scenarios.push(newScenario);
    self.currentScenario(newScenario);
});