views:

726

answers:

2

I'm attempting to create a navigation menu that uses some jQuery. I wanted keyboard users to be able to have the same experience as mouse users, so I'm duplicating the functionality found in my hover() event handler in my focus() and blur() event handlers. For some reason, this is causing noticeable lag in Firefox and IE when the user clicks on a link, which does not occur when the focus() and blur() code is taken out. How can I go about speeding this up? I've done as much optimizing as my limited javascript knowledge will permit, but I haven't seen any "speedup", so I'm thinking it's might be related to how these browsers handle the events.

Is there anything major I'm overlooking? Or are there any alternative ways to retain accessibility for keyboard users while not using these events?

        var statePad=0;

            function stateChanger(ourStatePad) {
                //the purpose of this function is to translate the current state of the menu into a different graphical representation of the menu state.
                var tempVar=ouStatePad;
                var tempArray = new Array;
                tempArray[5]=0;
                for (var x=0;x < 5;x++) {
                 tempArray[x]=tempVar % 10;
                 tempVar=(tempVar-tempArray[x])/10;
                }
                for (var arrayIndex=4;arrayIndex>=0;arrayIndex--) {
                   //Calculate the proper position in our CSS sprite, based on the each link's state, as well as it's neighbors'.
                    $(".block").eq(4 - arrayIndex)
                    .css(
                        "background-position",
                        //x-position
                        ((4 - arrayIndex) * -100) + "px " + 
                        //y-position
                        (tempArray[arrayIndex] + ((3 * tempArray[(arrayIndex) + 1]) * -30))) + "px";
                }
            }


        function hoverState(index,sign) {
            var placeholder=Math.pow(10,4-index);

            if (statePad != placeholder*2)
                statePad += (placeholder * sign);
            stateChanger(statePad);
}

        .click(function() {
            var index=$("#navbar a").index(this);
            statePad=Math.pow(10,(4-index))*2;
            stateChanger(statePad);
            $(".active").removeClass("active");
            $(this).addClass("active");
        })


        .hover(
         function () {
          hoverState($("#navbar a").index(this),1);
         },
         function () {
          hoverState($("#navbar a").index(this),-1);
         });

        $("#navbar a").focus( 
         function() {
          hoverState($("#navbar a").index(this),1);
         }
        );

        $("#navbar a").blur( 
         function() {
          hoverState($("#navbar a").index(this),-1);
         }
        ); 
    });

You can check it out here

+3  A: 

There's a lot of unnecessary lengthening of the scope chain in your code, and a longer scope chain will take longer to resolve. It could be shortened to the following

$("navbar a").click(blah) 
             .hover(foo,bar)
             .focus(foo)
             .blur(bar);

Hopefully this should result in less of a noticeable lag. If you still see noticeable lag after making this change, please post the code for the event handler functions as there may be improvements that can be made to that code too.

EDIT:

In response to your comment, you could get the index in the function using the passed in event object's target property, which will be the element that the event was raised on. So, to get the index of an <a> element in all the <a> elements in the <ul> with id navbar, we can use the fact that each <a> is contained within a <li>, therefore the index in each case will be the same. With this is mind, event.target will be the <a> element that the click event is raised on, event.target.parentNode will be the parent element of <a> which is the <li>

To get the index, you could use

function hoverState(e) { 
    // get the index of the <a> element, which will be the same
    // as the index of the <li> that contains it in the <ul>
    //
    var index = $(e.target.parentNode).prevAll().length; 
    //
    // get the sign
    var sign = (e.type === 'mouseenter' || e.type === 'focus')? 1 : -1;
}

This will remove the need for anonymous function event handlers wrapping hoverState.

Here's some reworked code

var statePad=0;

// the purpose of this function is to translate 
// the current state of the menu into a different 
// graphical representation of the menu state.
//
function stateChanger(ourStatePad) {

    var tempVar=ourStatePad;
    var tempArray = [0,0,0,0,0];
    for (var x=0;x < 5;x++) {
        tempArray[x]=tempVar % 10;
        tempVar=(tempVar-tempArray[x])/10;
    }
    // Calculate the proper position in our CSS sprite, 
    // based on the each link's state, as well as it's neighbors'
    //
    var arrayIndex=4;
    while (arrayIndex--) {

        $("#rightpostheader div.block").eq(4 - arrayIndex)
            .css(
                "backgroundPosition",
                //x-position
                ((4 - arrayIndex) * -100) + "px " + 
                //y-position
                (tempArray[arrayIndex] + ((3 * tempArray[(arrayIndex) + 1]) * -30))) + "px";
    }

}


function hoverState(e) {
    var index = $(e.target.parentNode).prevAll().length;
    var sign = (e.type === 'mouseenter' || 
                e.type === 'focus')? 1 : -1;
    var placeholder=Math.pow(10,4-index);

    if (statePad != placeholder*2)
        statePad += (placeholder * sign);
    stateChanger(statePad);
}

$("#navbar a")
    .click(function(e) {
        // might be able to rework this into using hoverState too
        var $this = $(e.target);

        // get the index of the parent <li>
        var index= $this.parent().prevAll().length;

        statePad=Math.pow(10,(4-index))*2;

        stateChanger(statePad);

        $("#navbar a").removeClass('active');
        $this.addClass('active');
    })
    .bind("mouseenter mouseleave focus blur", hoverState);
Russ Cam
And on top of that, closures are slow. Go with Russ. +1
cballou
I think I definitely oversimplified the code that I had posted before, so I posted the actual code I'm using. I don't know how to get the index of the current element without doing so in the context of an anonymous function. Would this still be considered unnecessary scope lengthening?
Rob
Wow...Enormous thanks for this in-depth response. I just had about 4 "ah-ha" moments when reading through your revision of my code. I didn't know about event access until now!
Rob
OK, I'm trying out your code, but it appears that using $(e.target).prevAll().length to get the index does not work like one would expect. It always returns 0, because the target in this case is actually the href that the issuing element refers to. After a bit of googling, I determined that you can get the ID with $(e.target.id), but I can't figure out the best way to determine the index just from knowing the ID. Any ideas?
Rob
Ah, my bad, I've realised we need to get the index of the `<a>` element in all `<a>` elements in `#navbar`. This is easily solved - since the index will be the same for the `<li>` elements that contain each `<a>` and `e.target` is the `<a>` element, simply amend `$(e.target).prevAll().length` to `$(e.target.parentNode).prevAll().length` and all will work fine. I will update the answer now with this amendment
Russ Cam
Yep, that works! It still doesn't solve the lag issue, though. I think firefox is just really slow when it comes to handling certain events! I genuinely appreciate all of your help me with this, though!
Rob
@Rob- Is it the actual JavaScript that is slow, or the swapping in and out of the CSS sprites? Have you console.logged the performance?
Russ Cam
By the way, the `$().load()` that you're doing in $(document).ready() is returning the full HTML document, not just the `<div>` that it looks like you're after
Russ Cam
I really don't think it's the sprites(although it seems logical that it would be bottleneck). But as you said, it's very fluid for the most part. Here's what happens, though - If one menu item is active, and I go to click on another, the "menu-sprite" updates and then there is enough of a delay before the "postheader-sprite" updates that it looks like it's a separate action. It's enough of a delay that I can easily get a <a href="http://imgur.com/Nya5v.png">screenshot</a> of it. If i don't bind focus and blur, it's seamless. Any links you can give me to learn about console logging?
Rob
Also, I'm aware of the $().load() thing. I was just playing with using Ajax methods to load in the postheader stuff on page load, but I already know that I'll need to do something else with that in the end.
Rob
A: 

I solved this by complete accident, sort of. I realized that my problem wasn't really the lag, that was just a symptom of a "conflict of events".

The problem as far as I can tell is that focus() is triggered either by tabbing over to a link or a mousedown() on an element that can receive focus. So, every time a link is clicked on, it's getting focus. But, a click() event isn't complete until the mouse is released. So the effect that I was seeing in Firefox and IE was a result of a slight delay between mousedown() and mouseup(). I tried just swapping out the .click() event handling in my code to mousedown(), and since it's just a single event that I was watching out for I decided to integrate that into my hoverState() function. I ended up with this:

function hoverState(e) {
    var index = $(e.target.parentNode).prevAll().length;
    if (e.type == 'mousedown') {
        statePad=Math.pow(10,(4-index))*2;
        $(".active").removeClass('active');
        $("#"+ e.target.id).addClass('active');
}
else {
   var sign = (e.type === 'mouseenter' || 
                 e.type === 'focus')? 1 : -1;
 var placeholder=Math.pow(10,4-index);
    if (statePad != placeholder*2)
        statePad += (placeholder * sign);
 $('h1').text(statePad + " " + e.type);
    }
    stateChanger(statePad);
}

$("#navbar a").bind("mouseenter mouseleave focus blur mousedown", hoverState);

However, this caused some bizarre behavior that screwed up the statePad variable. I reverted back to the code that Russ Cam provided me, and started rethinking things. I tried it out in Opera, which I hadn't done yet, and it worked fine. I tried it in Safari and Chrome, and they worked fine as usual. I tried it in Firefox, just trying to get a handle on what makes it different, and...it was working!

I looked back at my code, and it turns out that I was still binding the hoverState function to the mousedown() event. I'm not exactly sure why this works, but it does. It fixes the problem in IE, as well. Strangle, it causes a new issue in Chrome, it's so minor that I'm not even going to worry about.

I don't think I would have been able to resolve this without Russ' help, so I want to thank him again for all of his help in this.

Rob