3

In Windows Forms there's a BeginUpdate/EndUpdate pair on some controls. I want something like that, but for jQuery.


I have a div, that holds a div. like this:

<div id='reportHolder' class='column'>
  <div id='report'> </div>
</div>

Within the inner div, I add a bunch (7-12) of pairs of a and div elements, like this:

<h4><a>Heading1</a></h4>
<div> ...content here....</div>

The total size of the content, is maybe 200k. Each div just contains a fragment of HTML. Within it, there are numerous <span> elements, containing other html elements, and they nest, to maybe 5-8 levels deep. Nothing really extraordinary, I don't think. (UPDATE: using this answer, I learned there were 139423 elements in the fragment.)

After I add all the content, I then create an accordion. like this:

$('#report').accordion({collapsible:true, active:false});

This all works fine.

The problem is, when I try to clear or remove the report div, it takes a looooooong time, and I get 3 or 4 popups asking "Do you want to stop running this script?"

I have tried several ways:

option 1:

    $('#report').accordion('destroy');
    $('#report').remove();
    $("#reportHolder").html("<div id='report'> </div>");

option 2:

    $('#report').accordion('destroy');
    $('#report').html('');
    $("#reportHolder").html("<div id='report'> </div>");

option 3:

    $('#report').accordion('destroy');
    $("#reportHolder").html("<div id='report'> </div>");

after getting a suggestion in the comment, I also tried:

option 4:

    $('#report').accordion('destroy');
    $('#report').empty();
    $("#reportHolder").html("<div id='report'> </div>");

No matter what, it hangs for a long while.

The call to accordion('destroy') seems to not be the source of the delay. It's the erasure of the html content within the report div.

This is jQuery 1.3.2.

EDIT - fixed code typo.

ps: this happens on FF3.5 as well as IE8 .


Questions:

  1. What is taking so long?

  2. How can I remove content more quickly?


Addendum

I broke into the debugger in FF, during "option 4", and the stacktrace I see is:

data()
trigger()
triggerHandler()
add()
each()
each()
add()
empty()
each()
each()
(?)()    // <<-- this is the call to empty()
ResetUi()  // <<-- my code
onclick

I don't understand why add() is in the stack. I am removing content, not adding it. I'm afraid that in the context of the remove (all), jQuery does something naive. Like it grabs the html content, does the text replace to remove one html element, then calls .add() to put back what remains.

Is there a way to tell jQuery to NOT propagate events when removing HTML content from the dom?

In Windows Forms there's a BeginUpdate/EndUpdate pair on some controls. I want something like that, but for jQuery.


related:
jquery: fastest DOM insertion ?

Community
  • 1
  • 1
Cheeso
  • 189,189
  • 101
  • 473
  • 713
  • 1
    Just FYI: option 2 and option 3 are effectively the same. The second line of option 2 just gets the HTML content of the element and doesn't do anything with it. – Syntactic May 27 '10 at 18:55
  • 3
    `$('#report').html();` isn't what you want I don't think, to clear the content you need `$('#report').html('');` or `$('#report').empty();`, just calling `.html()` will *return* the content, but have no effect. – Nick Craver May 27 '10 at 18:55
  • Is is a browser specific problem, or browser agnostic? – Jeff Rupert May 27 '10 at 18:58
  • Why the `.accordion('destroy')`? Have you tried option 1 without it? I imagine if the containing div is removed from the DOM it'll destroy it. – mVChr May 27 '10 at 19:01
  • Thanks, the `$('#report').html()` was a typo. I actually used `$('#report').html('')` . – Cheeso May 27 '10 at 19:13

2 Answers2

3

You should try bypassing jQuery and empty out the content yourself after destroying the accordion:

$('#report')[0].innerHTML = '';

This will be much faster, but may cause IE to leak memory (jQuery goes to great pains to try to ensure that there are no references that prevent IE from doing garbage collection, which is part of why it is so slow to remove data).

C Snover
  • 17,908
  • 5
  • 29
  • 39
  • Shouldn't it be `$('#report').get(0)`? – Eric May 28 '10 at 14:13
  • wow, that's much faster. That takes ~1s, as compared to maybe 10s with `.empty()`. Ok, next question - how do I determine if the possible pitfall is happening, and how bad it is? – Cheeso May 28 '10 at 14:40
  • @Eric — `$('#report').get(0)` and `$('#report')[0]` are equivalent in jQuery. :) – C Snover May 30 '10 at 03:21
  • Yes, but the first will still work if jQuery change their implementation. – Eric May 30 '10 at 07:25
  • @Eric — jQuery will not change their implementation to remove array access. It is the write less, do more library. Beyond that, its internals strongly depend upon this syntax. – C Snover May 30 '10 at 07:29
1

I'm pretty sure the "why" will be revealed here (sourced from jQuery 1.4.2):

empty: function() {
  for ( var i = 0, elem; (elem = this[i]) != null; i++ ) {
    // Remove element nodes and prevent memory leaks
    if ( elem.nodeType === 1 ) {
      jQuery.cleanData( elem.getElementsByTagName("*") );
    }

    // Remove any remaining nodes
    while ( elem.firstChild ) {
      elem.removeChild( elem.firstChild );
    }
  }

  return this;
},

cleanData: function( elems ) {
  var data, id, cache = jQuery.cache,
      special = jQuery.event.special,
      deleteExpando = jQuery.support.deleteExpando;

  for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
    if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) {
      continue;
    }

    id = elem[ jQuery.expando ];

    if ( id ) {
      data = cache[ id ];

      if ( data && data.events ) {
        for ( var type in data.events ) {
          if ( special[ type ] ) {
            jQuery.event.remove( elem, type );
          } else {
            removeEvent( elem, type, data.handle );
          }
        }
      }

      if ( deleteExpando ) {
        delete elem[ jQuery.expando ];
      } else if ( elem.removeAttribute ) {
        elem.removeAttribute( jQuery.expando );
      }

      delete cache[ id ];
    }
  }
}

Notice that the empty() function (and anything else that removes the DOM element) will call cleanData() which scans each contained node to remove the expando property and unbind the events that were bound, remove all the data that was in the internal cache, and what not.

You could perhaps solve this problem by splitting up the work a little. I'm not sure how many children you have in that element but you could try something like this:

$('#report').children().each(function() {
  var $this = $(this); 
  setTimeout(function() { $this.remove(); }, 0);
});

Say there are 10 direct children of #report this will split the removing operation into 10 separate event loops, which might get around your long processing delay issue.

gnarf
  • 105,192
  • 25
  • 127
  • 161
  • Ah, thanks for the suggestion. Rather than splitting up the work (which I think would solve the problem, if I could do it), do you think there is a way to avoid the work entirely? Is there a way to add an HTML element to the DOM without adding it to jQuery? This would mean, there would be no events, no other stuff to do on removal. – Cheeso May 28 '10 at 13:52
  • 1
    @Cheeso — jQuery doesn’t add an expando to an element until it uses it, so if you use native DOM methods to add elements they will never have expandos. The problem is that jQuery **has** to check every element when they are removed, since that is the only way to know if an expando exists and need removing. If you don’t want this to happen, your only option is to use native DOM methods/properties like `innerHTML` and `removeChild`. – C Snover May 30 '10 at 05:45