views:

60

answers:

2

I'm trying to write some JS replicating jQuery's fadeIn and fadeOut functions. Here's the code I have so far:

function fadeIn(elem, d, callback)
{

    var duration = d || 1000;
    var steps = Math.floor(duration / 50);
    setOpacity(elem,0);
    elem.style.display = '';
    for (var i = 1; i <= steps; i++)
    {
        console.log(i/steps + ', ' + (i/steps) * duration);
        setTimeout('setOpacity("elem", '+(i / steps)+' )', (i/steps) * duration);
    }
    if (callback)
        setTimeout(callback,d);
}
function setOpacity(elem, level)
{
    console.log(elem);
    return;
    elem.style.opacity = level;
    elem.style.MozOpacity = level;
    elem.style.KhtmlOpacity = level;
    elem.style.filter = "alpha(opacity=" + (level * 100) + ");";
}

I'm having troubles with the first setTimeout call - I need to pass the object 'elem' (which is a DOM element) to the function setOpacity. Passing the 'level' variable works just fine... however, I'm getting "elem is not defined" errors. I think that's because by the time any of the setOpacity calls actually run, the initial fadeIn function has finished and so the variable elem no longer exists.

To mitigate this, I tried another approach:

setTimeout(function() { setOpacity(elem, (i / steps));}, (i/steps) * duration);

The trouble now is that when the function is called, (i/steps) is now always 1.05 instead of incrementing from 0 to 1.

How can I pass the object in question to setOpacity while properly stepping up the opacity level?

+2  A: 

Your first approach is evaluating code at runtime. You are most likely right about why it's failing (elem is not in the scope in which the code is eval'd). Using any form of eval() (and setTimeout(string, ...) is a form of eval()) is a general bad idea in Javascript, it's much better to create a function as in your second approach.

To understand why your second approach is failing you need to understand scopes and specifically closures. When you create that function, it grabs a reference to the i variable from the fadeIn function's scope.

When you later run the function, it uses that reference to refer back to the i from fadeIn's scope. By the time this happens however, the loop is over so you'll forever just get i being whatever it was when that loop ended.

What you should do is re-engineer it so that instead of creating many setTimeouts at once (which is inefficient) you instead tell your setTimeout callback function to set the next Timeout (or you could use setInterval) and do the incrementing if your values inside that callback function.

thomasrutter
+1 for setting timeout in timeout callback. the closure problem can be solved by using temp variable like `for(var i =0;....){ var t=i; setTimeout(t/*this will always take current loop index, not the last one*/...`
TheVillageIdiot
+2  A: 

Your "another approach" is correct, this is how it's usually done.

And as for the problem of i always being a constant, that's how closures work! You see, when you create this function that does something with i (like function() { alert(i); }), that function, as they say, 'captures', or 'binds' the variable i, so that variable i does not die after the loop is finished, but continues to live on and is still referenced from that function.

To demonstrate this concept, consider the following code:

var i = 5;
var fn = function() { alert(i); };

fn();  // displays "5"

i = 6;
fn();  // displays "6"

When it is written in this way, the concept becomes a bit more evident, doesn't it? Since you're changing the variable in the loop, after the loop is finished the variable retains it's last value of (1+steps) - and that's exactly what your function sees when it starts executing.

To work around this, you have to create another function that will return a function. Yes, I know, kind of mind-blowing, but bear with me. Consider the revised version of my example:

function createFn( theArgument )
{
    return function() { alert( theArgument ); };
}

var i = 5;
var fn = createFn( i );

fn();  // displays "5"

i = 6;
fn();  // still displays "5". Voila!

This works, because the fn function no longer binds the variable i. Instead, now it binds another variable - theArgument, which has nothing to do with i, other than they have the same value at the moment of calling createFn. Now you can change your i all you want - theArgument will be invincible.

Applying this to your code, here's how you should modify it:

function createTimeoutHandler( elemArg, iDivStepsArg )
{
    return function() { setOpacity( elemArg, iDivStepsArg ); };
}

for (var i = 1; i <= steps; i++)
{
    console.log(i/steps + ', ' + (i/steps) * duration);
    setTimeout( createTimeoutHandler( elem, i/steps ), (i/steps) * duration);
}
Fyodor Soikin
Thanks for the suggestion! Is this, as thomasrutter says, less efficient than doing it the way he says?
Mala
As for creating multiple setTimeouts, I completely agree: it would be more efficient to reinitialize after each iteration.
Fyodor Soikin
Ah ok, thanks then. +1 because your solution worked, but I'll accept thomasrutter's solution as i'll use that :)
Mala
I wasn't aiming for a solution so much as for letting you learn this important bit. Cheers! :-)
Fyodor Soikin