Search code examples
javascriptclosuresjqueryjquery-callback

Why does a parameter passed to my callback function in jQuery keep getting changed?


I'm working on different approaches to solving the well-known controversial removal of "requestAnimationFrame" in jQuery, where animate was reverted to use setTimeout/setInterval rather than the upcoming requestAnimationFrame API. This of course causes some known issues where animations get queued up when that page's browser tab is not in focus (Since I WANT this effect of animations halting when the page's tab is out of focus, it becomes a problem). One solution is to wrap everything in a cross-browser requestAnimFrame shim of the real "requestAnimationFrame" and another is to generate unique IDs for the animated element plus a timestamp and only run the animations when the window is focused.

This is a quick and dirty demo of the frustrating problem I've been having with the second approach of focus listeners and ID passing: http://jsfiddle.net/bcmoney/NMgsc/17

UPDATE (main problem solved): http://jsfiddle.net/bcmoney/NMgsc/

The barebones code:

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>window.focus listener EXAMPLE - jsFiddle demo by bcmoney</title> 
<script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js'></script>
<style type='text/css'>
  body { overflow:hidden; }
  #container { width:100%; height:100%; }
  #animated { width:120px; position:absolute; left:2px; top:20px; padding:50px; background:skyblue; color:white }
</style>
<script type='text/javascript'>//<![CDATA[ 
$(function(){
  var animation = 1;
  var startTime = undefined;
  var FPS = 60; //frames per second
  var AVAILABLE_WIDTH = $("#container").width() || "800px";
  var animation = null;
  var FOCUSED = false;
  var timestamp = new Date().getTime();
  var PAGE_ID = $("body").attr("id") + "-animated-"+timestamp;

  function continueScrolling(p) {
      console.log('FUNCTION p: '+p);
      if (FOCUSED === true && p === PAGE_ID) {
        animation = setTimeout(scrollRight.bind(p), FPS);
      } else {
          clearTimeout(animation);
          $('#animated').stop(true, true);
      }
  }

  var scrollRight = function(p) {
      time =  +new Date;
      startTime = (startTime !== undefined) ? startTime : time-FPS;
    move = ((time - startTime)/10 % AVAILABLE_WIDTH)+"px";
      console.log('P:'+p+' | T:'+time+' | ST:'+startTime+' | W:'+AVAILABLE_WIDTH+'\n'+move);
    $('#animated').animate({
         "left":move
      }, 
      1000/FPS, 
      "linear",
       function() {
         console.log('CALLBACK p: '+p);         
         continueScrolling(p);
       }
    );
  }  

  $(window).blur(function(){
     FOCUSED = false;
    $('#stop').click();
  });

  $(window).focus(function(){
     FOCUSED = true;
    $('#start').click();   
  });

  $('#start').click(function() {  
    scrollRight(PAGE_ID);
  });

  $('#stop').click(function() {
    $('#animated').stop(true, true);
    clearTimeout(animation);
  });
});//]]>  
</script>
</head>
<body id="slider">
  <a id="start" href="#start">Start</a> | <a id="stop" href="#stop">Stop</a>
<div id="container">
    <div id="animated">Animated content</div>
</div>
</body>
</html>

I've only recently realized the unique IDs may not be required and threw out my initial investigation of them with focus listeners, in favor of the shim approach for now; however this tinkering pointed out another potential problem with jQuery (or more likely my understanding thereof), passing parameters to a callback function in jQuery and ensuring the value of the parameter gets propagated through the context of the animation.

As you can see, the animation starts up and stops because the value "p" is no longer passed. I'm no expert on closures but I saw them proposed here as one solution as well as using $.proxy (which I tried from here before taking a break and posting to SO in frustration). I've never had this problem with $.ajax or $.getJSON callbacks, even when chaining API calls, but for some reason I can't seem to get the callback parameter to maintain its value for subsequent calls to the continueScrolling function. Any help from some JS/jQuery ninjas would be greatly appreciated...


Solution

  • setTimeout need a function for first argument:

            animation = setTimeout(function(){scrollRight(p)}, FPS);
    

    http://jsfiddle.net/K2nfM/