I am trying to use a closure to ensure that a function can only execute once. Sounds simple, and it works like this:
function runOnce(fn) // returns copy of fn which can only execute once
{
var ran = false;
return function()
{
if (!ran)
{
fn();
ran = true;
}
};
}
I have tested the function like so:
function lazyLoadGrid(event, ui)
{
alert('hi');
}
var test1 = runOnce(lazyLoadGrid);
var test2 = runOnce(lazyLoadGrid);
test1();
test2();
test1();
test2();
And it works as expected - 'hi' gets alerted exactly twice.
But then I try to use runOnce(lazyLoadGrid) as the callback to a jQuery UI event:
$('.accordion').each(function()
{
$(this).accordion({ autoHeight: false, change: runOnce(lazyLoadGrid) });
});
And madness ensues. What I expect is that each 'accordion' on the page will run lazyLoadGrid() exactly once, when that accordion is first opened. Instead, the closure callbacks seem to behave as if they are all referencing the same copy of 'ran'. lazyLoadGrid() runs the first time I open any accordion, and then never runs again for any other accordion. Logging the pre-condition value of 'ran' shows that it's 'true' every time I click any accordion after the first one.
What is the explanation for this? It may be worth noting I have an odd page, with nested accordions, and multiple jQuery UI tabs each containing accordions. To make matters worse, when I switch tabs the closure actually does run on the first-opened accordion of any given tab. Any advice is much appreciated.
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function.
In JavaScript, closures are defined as inner functions that have access to variables and parameters of outer function even after the outer function has returned.
It's important to understand that closures are created when functions are created, not when they are invoked. And because a closure was created when this setTimeout function was created, this enables the setTimeout function to access the i variable at whatever time the setTimeout function will run.
If JavaScript did not have closures, then more states would have to be passed between functions explicitly, making parameter lists longer and code noisier. So, if you want a function to always have access to a private piece of state, you can use a closure.
I believe the trouble you are having is because what you are thinking of as an "accordion" is actually a "panel". The accordion consists of all the panels in a group. It sounds like you want to run it once per panel, not once per accordion. The following demo illustrates the concept by including two accordions on a page. Notice that lazyLoadGrid()
is run twice, once for each accordion:
http://jsfiddle.net/cTz4F/
Instead what you want to do is create a custom event and call that event on each panel. Then you can take advantage of jQuery's built-in .one()
method which causes that an event handler is called exactly once for each element:
$('.accordion').accordion({
autoHeight: false,
change: function(e, ui) {
ui.newHeader.trigger("activated");
}
});
$('.accordion > h3').one("activated", lazyLoadGrid);
Working demo: http://jsfiddle.net/cTz4F/1/
How about:
function runOnce(fn) {
return function(){
fn();
fn = function(){};
}
}
// test
var foo = function(){
console.log('bar');
}
foo = runOnce(foo);
foo(); // bar
foo();
foo();
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With