Search code examples
javascriptjquerypromisestack-overflowjquery-deferred

Deferred chain crashing browser


This is small function that should be able to open and close a box. Opening and closing needs to take into account some CSS transitions, so I figured I can use $.Deferred.

Here's relevant code:

function Test(){

  // these are assigned Deferred objects during transitions
  this.opening = this.closing = false;

  this.isOpen = false;
  this.x = $('<div />').appendTo('body');
  this.x.width();
}

Test.prototype.open = function(){

  // box is already opening: return opening deferred
  if(this.opening)    
    return this.opening;

  // box is closing: this is the chain
  // that is supposed to wait for the box to close,
  // then open it again 
  if(this.closing)
    return this.closing.then((function(){
      return this.open();
    }).bind(this));

  // box is already open, resolve immediately
  if(this.isOpen)
    return $.when();    

  console.log('opening');
  this.opening = new $.Deferred();
  this.x.addClass('open');
  setTimeout((function(){
    this.opening.resolve();
    this.opening = false;
    this.isOpen = true;      
  }).bind(this), 1000);

  return this.opening;
};

The close() function is open() in reverse.

The problem appears when I try to close the box while it's being opened, or vice-versa. For example:

var t = new Test();

t.open(); // takes 1 second

// call close() after 0.05s
setTimeout(function(){
  t.close();
}, 50);

It appears there's a stack overflow happening or something like that. Does anyone know what's causing it?

The entire test code is here, but with a higher timeout value so it doesn't crash Chrome.


Solution

  • I notices several issues with your code:

    • returning deferred objects instead of promises, you can execute .then() only on promises

    • overriding deferred variable with bool value, I am using deferred.state() instead

    This is the updated version of your code:

    function Test(){
      this.opening = this.closing = false;
      this.isOpen = false;
      this.x = $('<div />').appendTo('body');
      this.x.width();
    }
    
    Test.prototype.open = function(){
      if(this.opening && this.opening.state() == 'pending')    
        return this.opening.promise();
    
      if(this.closing && this.closing.state() == 'pending')
        return this.closing.promise().then((function(){
          return this.open();
        }).bind(this));
    
      if(this.isOpen)
        return $.when();    
    
      console.log('opening');
      this.opening = new $.Deferred();
      this.x.addClass('open');
      setTimeout((function(){
        this.isOpen = true;    
        this.opening.resolve();
      }).bind(this), 1000);
    
      return this.opening.promise();
    };
    
    Test.prototype.close = function(){
      if(this.opening && this.opening.state() == 'pending') {
        console.log('opening is pending');
        return this.opening.promise().then((function(){
          console.log('opening is resolved');
          return this.close();
        }).bind(this));
      }
    
      if(this.closing && this.closing.state() == 'pending'){    
        console.log('closing is pending');
        return this.closing.promise();
      }
    
      if(!this.isOpen)
        return $.when();    
    
      console.log('closing');
      this.closing = new $.Deferred();
      this.x.removeClass('open');
      setTimeout((function(){
        console.log('closing resolved');
        this.closing.resolve();
        this.isOpen = false;
      }).bind(this), 1000);
    
      return this.closing.promise();  
    };
    
    var t = new Test();
    
    t.open();
    
    setTimeout(function(){
      t.close();
    }, 15);
    

    The output:

    "opening"
    "opening is pending"
    "opening is resolved"
    "closing"
    "closing resolved"