I have a couple of, what may end up being for this forum, overly-novice questions regarding unobtrusive event handling.
As I understand it, a properly set-up document would look something like this:
<html>
<head>
<title>Title</title>
<script src="jsfile.js" type="text/javascript></script>
</head>
<body>
//Body content, like some form elements in my case
</body>
</html>
Jsfile.js would look something like this:
function a() {
//code;
}
function b()...
window.addEventListener('load', a, false);
document.getElementById("id").addEventListener('click', b, false);
document.myForm.typeSel.addEventListener('change', c, false);
//or to use better browser-compatible code...
function addEvent(obj,evt,fn) {
if (obj.addEventListener)
obj.addEventListener(evt,fn,false);
else if (obj.attachEvent)
obj.attachEvent('on'+evt,fn);
}
addEvent(window, 'load', a);
addEvent(document.getElementById('id'), 'click', b);
addEvent(document.myForm.typeSel, 'change', c);
As I understand it, while in the head the browser will load this JavaScript code, adding each of those event handlers to their respective elements. HOWEVER... While the window handler is added properly, none of the others are. But if placed within a function, the (for instance) getElementById method of accessing an element works just fine, and the event handler is added. So I could conceivably make a loadEvents() function which is called via window onload, which contains all of the addEvent() functions for the other document elements for which I need event handlers. But as I understand the whole thing, I shouldn't have to do this.
In addition, if I were to stick the addEvent code within the body along with the element it addresses, such as:
<input type="checkbox" id="test" />
<script type="text/javascript>
document.getElementById("test").onclick = func;
</script>
...then it works fine. But of course it also violates the whole reason for removing inline event handlers!
So question being: In order to use *element*.addEventListener('click',func,false), addEvent(*element*,'click',func), or even *element*.onclick = func - how can I successfully reference an element at the end of a script file in the head, without having to stick it in another function? Why does getElementById and other such methods not work outside of a function in the head?
Or, is there some flaw in my underlying understanding?
Putting <script> in the <head> used to be the wisdom. But nowadays, with heavy ajax pages, <script> is more and more often but in the body, as far down below as possible. The idea is that the loading and parsing of the <script> keeps the rest of the page from loading, so the user will be looking at a blank page. By making sure the body is loaded as fast as possible, you give the user something to look at. See YAHOO best practices for a great explanation on that issue: http://developer.yahoo.com/performance/rules.html
Now, regardless of that issue, the code as you set it up now, can't work - at least, not when the elements you attempt to attach the handlers to aren't created yet. For example, in this line:
document.getElementById("id").addEventListener('click', b, false);
you will get a runtime error if the element with id="id" is inside the body. Now, if you put the <script> in the body, way below, after the content (including the lement with id="id", it will just work, since the script is executed after the html code for those elements is parsed and added to the DOM.
If you do want to have the script in the head, then you can do so, but you'll need to synchronize the adding of the event handlers with the rendering of the page content. You could do this by adding them all inside the document or window load handler. So, if you'd write:
//cross browser add event handler
function addEventHandler(obj,evt,fn) {
if (obj.addEventListener) {
obj.addEventListener(evt,fn,false);
} else if (obj.attachEvent) {
obj.attachEvent('on'+evt,fn);
}
}
addEventHandler(document, 'load', function(){
//set up all handlers after loading the document
addEventHandler(document.getElementById('id'), 'click', b);
addEventHandler(document.myForm.typeSel, 'change', c);
});
it does work.
The reason why window.addEventListener works while document.getEle...().addEventListener does not is simple: window object exists when you're executing that code while element with id="abc" is still not loaded.
When your browser downloads page's sources the source code is parsed and executed as soon as possible. So if you place script in head element - on the very beginning of the source - it's executed before some <div id="abc">...</div> is even downloaded.
So I think now you know why
<div id="test">Blah</div>
<script type="text/javascript">document.getElementById("test").style.color = "red";</script>
works, while this:
<script type="text/javascript">document.getElementById("test").style.color = "red";</script>
<div id="test">Blah</div>
doesn't.
You can handle that problem in many ways. The most popular are:
putting scripts at the end of document (right before </body>)
using events to delay execution of scripts
The first way should be clear right now, but personally I prefer last one (even if it's a little bit worse).
So how to deal with events? When browser finally download and parse whole source the DOMContentLoaded event is executed. This event means that the source is ready, and you can manipulate DOM using JavaScript.
window.addEventListener("DOMContentLoaded", function() {
//here you can safely use document.getElementById("...") etc.
}, false);
Unfortunately not every browser support DOMContentLoaded event, but as always... Google is the anwser. But it's not the end of bad news. As you noticed addEventListener isn't well supported by IE. Well... this browser really makes life difficult and you'll have to hack one more thing... Yes... once again - Google. But it's IE so it's not all. Normal browsers (like Opera or Firefox) supports W3C Event Model while IE supports its own - so once again - Google for cross-browser solution.
addEventListener might seems now the worst way to attach events but in fact it's the best one. It let you easly add or remove many listeners for single event on single element.
PS. I noticed that you consider of using Load event to execute your scripts. Don't do that. Load event is execute too late. You have to wait till every image or file is loaded. You should use DOMContentLoaded event. ;)
EDIT:
I've forgotten... dealing with cross-browser event model is much easier when you're using some framework like very popular jQuery. But it's good to know how the browsers work.
are you familiar with jQuery?
its a javascript library featuring some really awesome tools.
for instance if you want to have some js action done just after your page if fully loaded and all DOM elements are created (to avoid those annoying exceptions) you can simply use the ready() method.
also i see you want to attach click \ change events jQuery takes care of this too :) and you don't have to worry about all those cross-browser issues.
take a look at jQuery selectors to make your life easier when attempting to fetch an element.
well thats it, just give it a shot, its has a very intuitive API and a good documentation.
Related
I want to invoke jquery.animate directly to change the effect of a div, but found it doesn't have any effect.
Instead, I need to put it inside a setTimeout(..., 0) to make it work.
I wonder why do I need to do this, and is it the best approach?
Live demo
http://jsbin.com/docahu/2/edit
Or here:
var FooView = Backbone.View.extend({
id: 'foo',
});
var BarView = Backbone.View.extend({
render: function() {
$("#foo").animate({width: '200px'});
// !!! HERE !!!
setTimeout(function() {
$("#foo").animate({height: '100px'});
}, 0);
return this;
}
});
var fooView = new FooView();
var barView = new BarView();
var combinedView = $(fooView.render().el).append(barView.render().el);
$(document.body).append(combinedView);
#foo {
width: 50px;
height: 50px;
background-color: blue;
color: white;
}
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="Jquery animate delay problem in backbone render" />
<script src="//code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="//jashkenas.github.io/underscore/underscore-min.js"></script>
<script src="//jashkenas.github.io/backbone/backbone-min.js"></script>
<meta charset="utf-8">
<title>JS Bin</title>
</head>
<body>
</body>
</html>
You can see height is changed but the width is not.
PS:
Also I found $(document).ready() is also working:
$(document).ready(function() {
$("#foo").animate({height: '100px'});
});
Which one is better to use?
It's because it's trying to animate the width before the element is in the DOM. If you put a selector in that position, you'll probably find it's not getting anything.
Doing a timeout of 0 (so javascript finishes rendering the things THEN tries the animation) or waiting for the document to finish rendering fixes that
Things happen in this order:
You render your view, but it's unattached to the DOM
the width animation runs. Nothing happens because '#foo' isn't on the DOM.
you attach it to the dom.
your height animation runs. It works because '#foo' is in the DOM.
Well seems like it works by chance. The reason the first one is not working is probably because the object are still not loaded on the screen. The second one is working because after the timer was dispatched and ended (this does not really take 0 time) the page was loaded by another thread on the computer. So the overhead of creating the timer and calling back the procedure is apparently enough to finish loading the page.
You should use $(document).ready to make sure it is always called after the document is fully loaded, because like I said, it is now working by chance, and a different browser\machine may not run any of the two (or both).
Background: JavaScript starts getting executed while the page is loaded, and the DOM is build at the same time (just at the time the HTML and JavaScript text is downloaded). So if you reference DOM objects from JavaScript code like you are doing now, you get a race condition where the outcome is not defined. To avoid that there is the $(document).ready callback.
Edit
See this question. Also the Udacity course is really cool to understand what is going on.
Updated
TL;DR
Using $(document).ready is equivalent to placing the JavaScript at the end of the document…
JSBin (which is were the OP posted his sample code) will execute the JavaScript after all the HTML elements render, but before the $(document).ready event. Binding the jQuery.animate in $(document).ready is the same as firing it anytime after the view is appended to the DOM.
The simple and stable solution is to simply invoke
$("#foo").animate({width: '200px'});
on the last line of the OP's code. (Read the end of the Answer to see a more formal way of binding the jQuery.animate function)
To answer the OP's original quesiton: setTimeout() works in your case because of the way async functions are queue in the JavaScript runtime. If the <body> has already been loaded, then using setTimeout(0) the way the OP does, will have the same effect as placing the animation binding in $(document).ready.
Why setTimeout(0) works
The first thing to understand is that JavaScript is not a multi-threaded framework. While you certainly can invoke non-synchronous functions, asynchronous, async functions don't actually run parallel to the synchronous operations. Instead, async functions are queued to run as soon as the runtime is free.
For example, take the following three synchronous functions.
function1();
function2();
function3();
As you'd expect function1will fire first, followed, in order, byfunction1, function2thenfunction3. However, if I place function1` in an asynchronous call
setTimeout(function1,0);
function2();
function3();
then function1 will be placed on a queue, leaving function2 and then function3 to fire. As soon as the event loop is finished function1 is invoked. That is, it fires last! You can see this in action in this fiddle.
In the OP's example, setTimeout(function() { $("#foo").animate({height: '100px'});}, 0); was fired immediately after the runtime executed $(document.body).append(combinedView); and so jQuery was able to find the #foo element, so technically this is a correct way to do what the OP wants. This is true because of the way JSBin works. That is, it loads the JavaScript from its JavaScript Module after the DOM has loaded (but before the $(document).ready event).
Edited:
Do not use the $(document).ready function...in general
I think there's some confusion regarding how $(document).ready function solves the OP's problem. Most of the confusion probably stems from the complexity of how different web page elements (HTML, CSS, JavaScript) affect the rendering of the DOM.
There is a main parsing and rendering thread used by browsers. This is where your HTML is processed, your CSS stylesheets are fetched and parsed, and your JavaScript is fetched/parsed. All of these operations are executed as they are encountered and will be blocking (unless async or defer is specified in your <link>/<script> tags).
The order in which you pace all of these tags is essential. If your script tag is written at the top of your document (say within the <head> tag) it will be executed before any HTML is injected into the DOM.
In essence, using $(document).ready is equivalent to placing the JavaScript at the end of the document… Since JSBin (which is were the OP posted his sample code) will execute the JavaScript after all the HTML elements render, but before the $(document).ready function, binding the jQuery.animate in $(document).ready is the same as firing it anytime after the view is appended to the DOM.
Instead, the simple and stable solution is to simply invoke
$("#foo").animate({width: '200px'});
after both the fooView and the barView have been attached to the DOM. Or more formally:
var BarView = Backbone.View.extend({
render: function() {
// process your html here
return this;
}
bindTransitions: function {
$("#foo").animate({width: '200px', height: '100px'});
}
});
var fooView = new FooView();
var barView = new BarView();
var combinedView = $(fooView.render().el).append(barView.render().el);
$(document.body).append(combinedView);
barView.bindTransitions();
If you've properly scoped your backbone view, you should be able to reference the element that is currently in memory when you are trying to change the width or height (pre-render).
You can do this by doing something similar to: this.$el.find(#foo") to obtain access / manipulate to your markup before it is added to the DOM.
I am just starting out with JavaScript and I have a simple code that sends a value to an element with id p. I am currently declaring this function in a <script> in the <head> element of my document.
function writeP(resultSet) {
document.getElementById('p').innerHTML = resultSet.length;
};
writeP(results);
When I have this listed within the <head> element and run the webpage, firebug throws this error at me: TypeError: document.getElementById(...) is null.
However, if I move the code block into a <script> tag beneath the element and then reload the webpage, no problems and the script works as it should. Is there any reason for this, and a way I could make this work so I wouldn't have to define my functions beneath the element or include a onload on my body element?
Thanks for your help
Reason is that by the time your launch js code, DOM is not yet prepared, and JS can't find such element in DOM.
You can use window.onload (docs on W3schools) trigger to fire your functions after all elements are ready. It's same as having onload property on body element, but is more clear, as you can define it in your js code, not in html.
JS evaluates syncronically. Therefore, it does matter WHEN you declare the function. In this case, you're declaring it before the element actually exists.
Second, when you declare a function with that syntax, it does get eval'd inmediately. If you declared, instead
var writeP=function(resultSet) {
document.getElementById('p').innerHTML = resultSet.length;
};
you could save just the call to the end of the Doc, and leave the declaration at the beggining.
However, I would advise you to read a few jQuery tutorials to learn easier ways to deal with dom manipulation. Nobody runs raw JS for that task anymore.
jQuery includes an useful call to document ready event, which will save you a lot of headaches and is -IMHO- more efficient than the onload event. In this case, you would include the jQuery library somewhere in your code
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
and then add
<script>
jQuery(document).ready(function() {
var writeP=function(resultSet) {
jQuery('#p').html(resultSet.length);
};
writeP(resultSet);
});
</script>
just about anywhere in your document or an external js file, as it suits you.
So, I need to know the width of an element with javascript, the problem I have is that the function fires too early and the width changes when the css is tottally applied. As I understood, the $(document).ready() function was fired when the document is completed, but it doesn't seem to work like that.
Anyways, I'm sure that with the code my problem will be understood (this is a simplified example):
<html>
<head>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<link href='http://fonts.googleapis.com/css?family=Parisienne' rel='stylesheet' type='text/css'>
<style type="text/css">
#target {
font-family: 'Parisienne', cursive;
float: left;
}
</style>
</head>
<body>
<div id="target">Element</div>
</body>
</html>
<script type="text/javascript">
$(document).ready(function(){
console.debug($('#target').outerWidth());
alert('hold on');
console.debug($('#target').outerWidth());
});
</script>
I want to know the width of the #target div, the problem is that the code that's executed before the alert gives a different output than the one after, presumably because the font is not fully loaded and it's measuring the div with the default font.
It works as I expect in Google Chrome, but it doesn't on IE and Firefox.
If you rely on external content to be already loaded (e.g. images, fonts), you need to use the window.load event
$(window).on("load", function() {
// code here
});
The behaviour of these events is described in this article:
There is [a] ready-state however known as DOM-ready. This is when the browser has actually constructed the page but still may need to grab a few images or flash files.
Edit: changed syntax to also work with jQuery 3.0, as noted by Alex H
Quote OP:
"As I understood, the $(document).ready() function was fired when the document is completed,"
$(document).ready() fires when the DOM ("document object model") is fully loaded and ready to be manipulated. The DOM is not the same as the "document".
W3C - DOM Frequently Asked Questions
You can try $(window).load() function instead...
$(window).load(function() {
// your code
});
It will wait for all the page's assets (like images and fonts, etc.) to fully load before firing.
The jQuery .ready() function fires as soon as the DOM is complete. That doesn't mean that all assets (like images, CSS etc) have been loaded at that moment and hence the size of elements are subject to change.
Use $(window).load() if you need the size of an element.
The "ready" event fires when the DOM is loaded which means when it is possible to safely work with the markup.
To wait for all assets to be loaded (css, images, external javascript...), you'd rather use the load event.
$(window).load(function() {
...
});
You could use $(window).load(), but that will wait for all resources (eg, images, etc). If you only want to wait for the font to be loaded, you could try something like this:
<script type="text/javascript">
var isFontLoaded = false;
var isDocumentReady = false;
$("link[href*=fonts.googleapis.com]").load(function () {
isFontLoaded = true;
if (isDocumentReady) {
init();
}
});
$(document).ready(function () {
isDocumentReady = true;
if (isFontLoaded) {
init();
}
});
function init () {
// do something with $('#target').outerWidth()
}
</script>
Disclaimer: I'm not totally sure this will work. The <link> onload event may fire as soon as the stylesheet is parsed, but before its external resources have been downloaded. Maybe you could add a hidden <img src="fontFile.eot" /> and put your onload handler on the image instead.
I have absolutely, repeatably seen the same problem in IE9 and IE10. The jquery ready() call fires and one of my <div>'s does not exist. If I detect that and then call again after a brief timeout() it works fine.
My solution, just to be safe, was two-fold:
Append a <script>window.fullyLoaded = true;</script> at the end of the document then check for that variable in the ready() callback, AND
Check if $('#lastElementInTheDocument').length > 0
Yes, I recognize that these are nasty hacks. However, when ready() isn't working as expected some kind of work-around is needed!
As an aside, the "correct" solution probably involves setting $.holdReady in the header, and clearing it at the end of the document. But of course, the really-correct solution is for ready() to work.
The problem $(document).ready() fires too early can happen sometimes because you've declared the jQuery onReady function improperly.
If having problems, make sure your function is declared exactly like so:
$(document).ready(function()
{
// put your code here for what you want to do when the page loads.
});
For example, if you've forgotten the anonymous function part, the code will still run, but it will run "out of order".
console.log('1');
$(document).ready()
{
console.log('3');
}
console.log('2');
this will output
1
3
2
Disclaimer: I am new to jQuery.
I am trying to implement a fadeOut effect in jQuery for a div block, and then fadeIn effect on two other div blocks.
However, these effects are only working in the Chrome browser (i.e. they won't work in Safari, FireFox, Opera) which is rather perplexing to me. I have tried clearing my cache in case it was storing an old file, but none of that seemed to do anything.
Basic idea (stored in mainsite.js file):
$("#videoThumbnail_XYZ").click(function () {
$("#thumbnailDescription_XYZ").fadeOut(300);
$("#videoPlayer_XYZ").delay(300).fadeIn(100);
$("#videoHiddenOptions_XYZ").delay(300).fadeIn(100);
});
So when a div tag with the id of videoThumbnail_XYZ is clicked, it starts the fadeOut and fadeIn calls on the other div tags.
I am loading my javascript files into the page in this order (so jQuery is loaded first):
<script src="http://code.jquery.com/jquery-1.4.4.js"></script>
<script async="" type="text/javascript" src="javascripts/mainsite.js"></script>
Any guidance you could give is greatly appreciated!
Make sure the DOM is fully loaded before your code runs.
A common way of doing this when using jQuery is to wrap your code like this.
$(function() {
$("#videoThumbnail_XYZ").click(function () {
$("#thumbnailDescription_XYZ").fadeOut(300);
$("#videoPlayer_XYZ").delay(300).fadeIn(100);
$("#videoHiddenOptions_XYZ").delay(300).fadeIn(100);
});
});
This is a shortcut for wrapping your code in a .ready() handler, which ensure that the DOM is loaded before your code runs.
If you don't use some means of ensuring that the DOM is loaded, then the #videoThumbnail_XYZ element may not exist when you try to select it.
Another approach would be to place your javascript code after your content, but inside the closing </body> tag.
<!DOCTYPE html>
<html>
<head><title>your title</title></head>
<body>
<!-- your other content -->
<script src="http://code.jquery.com/jquery-1.4.4.js"></script>
<script async="" type="text/javascript" src="javascripts/mainsite.js"></script>
</body>
</html>
If mainsite.js is being included before your div is rendered, that might be throwing the browsers for a loop. Try wrapping this around your click handler setup:
$(document).ready(function(){
// your function here
});
That'll make sure that isn't run before the DOM is ready.
Also, you might consider putting the fadeIn calls in the callback function of your fadeOut, so if you decide to change the duration later on, you only have to change it in one place.
The way that'd look is like this:
$("#thumbnailDescription_XYZ").fadeOut(300,function(){
$("#videoPlayer_XYZ").fadeIn(100);
$("#videoHiddenOptions_XYZ").fadeIn(100);
});
I see you have a delay set to the same duration your fadeOut is, I would recommend instead of delaying which in essence your waiting for the animation to complete that instead you use the callback function.
$("#videoThumbnail_XYZ").click(function () {
$("#thumbnailDescription_XYZ").fadeOut(300, function() {
$("#videoPlayer_XYZ").fadeIn(100);
$("#videoHiddenOptions_XYZ").fadeIn(100);
});
});
While JavaScript provides the load event for executing code when a page is rendered, this event does not get triggered until all assets such as images have been completely received. In most cases, the script can be run as soon as the DOM hierarchy has been fully constructed. The handler passed to .ready() is guaranteed to be executed after the DOM is ready, so this is usually the best place to attach all other event handlers and run other jQuery code.
$(document).ready(function(){
$("#videoThumbnail_XYZ").click(function () {
$("#thumbnailDescription_XYZ").fadeOut(300);
$("#videoPlayer_XYZ").delay(300).fadeIn(100);
$("#videoHiddenOptions_XYZ").delay(300).fadeIn(100);
});
});
All three of the following syntaxes are equivalent:
* $(document).ready(handler)
* $().ready(handler) (this is not recommended)
* $(handler)
I have hunted around for answer to this one, and though have found related quesions, I couldn't quite find an exact match for this.
I have a fairly large app which is supposed to load pages into divs in another page using the jQuery.load() method. The problem I have is that when loading the same page over and over again into the same div, I see the memory of the browser increase substantially (memory leak). If I call $("*").unbind, I of course do not see a leak, but then everything has been reset, so this isn't reallya fix. The following code example reproduces this problem:
Test1.htm
<head>
<title></title>
<script type="text/javascript" language="javascript" src="Scripts/jquery-1.3.2.js"></script>
<script type="text/javascript" language="javascript">
<!--
var i = 0;
$(document).ready(function() {
$("#btn").click(
function() {
i++;
$("#Test1").load("Test2.htm", null, function() {
//$(document).trigger("test");
})
$("#count").html(i);
});
});
//-->
</script>
</head>
<body>
<img id="btn" src="someimage.png" />
<h3>We are loading Test2.htm into below div</h3>
<div>
Count loads =<span id="count">0</span>
</div>
<div id="Test1" style="border-style:solid">EMPTY</div>
</body>
Test2.htm = any old html page..
If you load Test1.htm and click the button mutliple times, you'll notice the browser memory steadily increasing. I believe the problem is that the loaded js and DOM elements are never set for garbage collection. In my real world system I have tried removing (elem.remove() or .empty()) the loaded elements, but this doens't really fix the problem. I also have many js files loaded using "src", which I replaced with $.getScript, this seems to have had made a small improvement. These are all workarounds thought, and I wanted to find a real solution for this problem. Any ideas?
Edit: update due to more info provided about test2.htm (the page being loaded)
Original answer (for historical purposes): I don't actually see any leaks in the code/markup you have provided - is it possible the leak is in Test2.htm (which you haven't provided the code/markup for)?
New answer:
I would suggest that it it probably due to either multiple loads of jQuery, or other scripts you have in test2.htm.
Assuming jQuery does not leak by simply instantiating and then nullifying jQuery and $, loading multiple times will keep at least 2 copies of jQuery in memory. When loaded, jQuery keeps a backup of any previous versions of $ and jQuery in _$ and _jQuery - so you are going to have at least 2 copies of jQuery loaded when you use load() multiple times.
The above assumption is most likely not correct however - there is every chance that jQuery has leaks even if you "unload" it by setting $,jQuery,_$ and _jQuery to null - it's not really intended to be loaded multiple times like that (however I'm sure that they allow it intentionally, so you can use noConflict() to load and use two different versions of jQuery if necessary).
You can add a "selector" to a load URL. For example:
$("#Test1").load("Test2.htm body", null, function() {
//callback does nothing
});
//or
$("#Test1").load("Test2.htm div#the_Div_I_Want", null, function() {
//callback does nothing
});
I would suggest doing this if you are not interested in any scripts in the ajax result, or alternatively if you do want scripts, you'd need to choose a selector to disable only certain elements/scripts, e.g.
/* load with selector "all elements except scripts whose
src attribute ends in 'jquery.js'" */
$("#Test1").load("Test2.htm :not(script[src$='jquery.js'])", null, function() {
//callback does nothing
});
Also of note is that if you leave out the "data" argument (you have it as null), and provide a function as the second argument, jQuery will correctly determine that the second argument is the callback, so
$("#Test1").load("Test2.htm :not(script[src$='jquery.js'])", function() {
//callback does nothing
});
is acceptible
Hmm perhaps it's just something really basic, but if i set $.ajaxSetup({ cache: false }); before the load calls, I don't seem to get the problem. Now, of course my "real" code has this call, so why might I see a problem? I believe the Tabs UI extension is causing caching to be switched on (I don't actually believe this, but invoking the false cache call before each load seems to fix it!!)
Ok so I finally found the problem and it's not a leak at all (which I suspected), it's simply the result of attaching multiple very complex handlers to the same trigger/event. I raised this question relating to that:
JQuery event model and preventing duplicate handlers