Backbone: abort previous router callback execution

251 views Asked by At

Suppose I have 2 links on my page:

<a href="#A">Link A</a>
<a href="#B">Link B</a>

And I use backbone router to handle in-app page navigation.

On routing link '#A', I have registered a listenerA and similarly listenerB for '#B' where both listenerA and listenerB do some heavy task in background.

Now, let's say a user clicks 'Link A' and with gap of few milliseconds, clicks 'Link B'.

So, what happens is that when listenerA is done with its execution, only then listenerB starts executing due to single UI thread.

Is there a way, I can block/abort/preempt execution of listenerA if listenerB is requested?

It's expected that a user will like to see response for his latest action. Previous actions should not be honored if the two actions are similar.

2

There are 2 answers

0
vvahans On

To understand how Backbone handles routes lets review some chunk of code from Backbone.History:

 if (this._hasPushState) {
        Backbone.$(window).on('popstate', this.checkUrl);
      } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
        Backbone.$(window).on('hashchange', this.checkUrl);
      } else if (this._wantsHashChange) {
        this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
      }

If you are using modern browser the 2nd if black will subscribe to hashChange event and any time you will navigate from link to link it will call checkUrl which will manage call to appropriate action.

So direct answer to your queastion will be no, you can't.

But you can use a few workarounds depending what you doing in your actions.


1st solution.

Use setTimeout() or similar _.debounce() function. Select all links which need to be protected or single treathed and navigate to needed url mnually:

$(document.body).on('click', 'a:not([reference-to-out])', function(e){
   e.preventDefault();
   if (app.linkTimeoutId) {
      clearTimout(app.linkTimeoutId);
   } 

   app.linkTimeoutId = setTimeout(function () {
          app.linkTimeoutId = null;           
          app.router.navigate(e.currentTarget.pathname, {trigger: true});
   }, 500)

 });

2nd solution

If your heavy tasks are network related (fetching collection, doing another requests to remote), track them keeping in another object and cancel them when Backbone.Router fires 'route' event. This will prevent a lot of needless operations.

3rd solution

I was thinking about $.Deferred objects, but in this case you should wrap all your controller's actions with it and also keep track of them.

0
nikoshr On

You can alter how Backbone handles the registration of routes to introduce a delay between a route match and the execution of its callback. For example,

var Router = Backbone.Router.extend({
    routes: {
        ":id" : "go"
    },

    route: function(route, name, callback) {
        var router = this;
        if (!callback) callback = this[name];

        var f = function() {
            var args = _.toArray(arguments);
            console.log('Clicked route', args[0]);

            if (router.timer)
                clearTimeout(router.timer);

            router.timer = setTimeout(function() {
                callback.apply(router, args);
            }, 1000);

        };
        return Backbone.Router.prototype.route.call(router, route, name, f);
    },

    go: function(id) {
        console.log("Routed "+id);
    }
});


var r = new Router();
Backbone.history.start();

Navigating to a route will set a timer. If another navigation occurs in the given lapse of time, the timer is canceled and the action aborted.

See http://jsfiddle.net/nikoshr/0ws8d0ky/ for a demo