Backbone.Model how to listen for changes on multiple attributes only once

6.4k views Asked by At

Say I have a Layout model with width and height properties. And Say I want to update a view when either of them changes, but the updating process in computational intensive and requires the value of both width and height. The computation process also updates the ratio property of the model.

If I do

this.listenTo(this.model, 'change:width change:height', this.doLayout);

I will end up with two doLayout calls in the worst case and both will do the same, wasting resources.

If I do

this.listenTo(this.model, 'change', function(model) {
    if(model.hasChanged('width') || model.hasChanged('height')) {
        this.doLayout();
    }
});

On first sight it looks like I solved the problem of doing the doLayout calculations twice. But the way Backbone.Model works is that since doLayout sets ratio, I will end up with a second change event. The changedAttributes for the first event is {width: ..., height: ...} and for the second one it's {width: ..., height: ..., ratio: ...}. So yeah, doLayout is executed twice again...

Any solution other than rewriting the set method?

Edit: Note that I need a general purpose solution. Hard-coding the width/height/ratio special case is not acceptable. In reality I have many more computed properties which are updated based on others and the way Backbone handles the change event does not work well in this situation.

2

There are 2 answers

2
loganfsmyth On

The easiest thing to do, for me anyway, is to change the way you think about the events. For instance, in this case, it isn't that you want to only do it once, it is that you want to do something after both have finished.

The easiest way to do that here would be to debounce the event:

this.listenTo(this.model, 'change:width change:height', _.debounce(this.doLayout, 1));

That way it will only run once, even if it is called a bunch of times synchronously.

Note: The exact timing behavior of _.debounce(..., 1) depends on the browser's treatment of very small timeouts. If immediate synchronous-like behavior is important, you could replace the usage of _.debounce with a custom debouncer written using setImmediate.

0
Prinzhorn On

I came up with a great solution which shouldn't have any side effects.

Backbone.Mode.extend({
    set: function() {
        this.changed = {};
        Backbone.Model.prototype.set.apply(this, arguments);
    }
});

If you look at the Backbone source and check where this.changed is used it should be clear what this does. The change:[attr] type of events aren't affected at all. But within the change event both changedAttributes() and hasChanged() will only return the properties that where changed by the very last set call.

I consider submitting a pull-request to Backbone which makes this configurable as an option to set.