1

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.

Prinzhorn
  • 22,120
  • 7
  • 61
  • 65
  • When I need to control change events firing, I almost always write my own wrapper to set. – kinakuta Nov 17 '14 at 18:28
  • Sounds like I will do this as well. But triggering a custom change event means duplicating things like `hasChanged`, `changedAttributes` etc. – Prinzhorn Nov 17 '14 at 18:30

2 Answers2

4

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.

loganfsmyth
  • 156,129
  • 30
  • 331
  • 251
  • Thanks. Introducing an artificial delay is not an option though. Using debounce feels like covering the issue with some makeup without actually solving it. In reality I have properties which are updated at 60 fps, rendering performance has absolute priority. It performs well, but could be better (in some cases I get 3 or 4 change events and thus unnecessary renderings). – Prinzhorn Nov 17 '14 at 18:26
  • If `debounce` is too slow (since it uses `setTimeout`), you could look into debouncing using `setImmediate` and its browser polyfills. That would queue the function to call at the end of the current event-loop pass, so you would not even incur an event-loop tick. In those cases, I'd be surprised if this introduced significant delays. – loganfsmyth Nov 17 '14 at 19:04
0

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.

Prinzhorn
  • 22,120
  • 7
  • 61
  • 65