Skip to content

Instantly share code, notes, and snippets.

@adambankin
Last active October 27, 2016 21:33
Show Gist options
  • Save adambankin/1db497d806fef3aff86358647a2703f0 to your computer and use it in GitHub Desktop.
Save adambankin/1db497d806fef3aff86358647a2703f0 to your computer and use it in GitHub Desktop.
Explaining setTimeout and requestAnimationFrame

Single-threaded

JavaScript runtimes give each window access to one "main" CPU thread. An iframe creates a new main window and therefore gets its own thread. When the main thread is busy running code it can't run other code, so it queues it to be run when the current code has completed.

Call Stack

This is a synchronous stack of procedures filled with the current running procedure and the ancestry of procedures that directly spawned it.

By "procedure" I mean things like: calling functions, assigning values, evaluating conditions, etc…

Things to note

  • The top of the stack is the current procedure.
  • The base of the stack is the procedure that started the current run.
  • If a procedure calls another procedure (like a function calling another function) then the new function is added to the top of the call stack.
  • The procedures inbetween the base and the top are a direct line of procedures that link the base to the top.
  • When a procedure completes it is removed from the top of the stack until the stack is empty.

Example

	function doOtherStuff (time) {
		console.log(time);
	}

	function doStuff () {
		var startTime = Date.now();

		doOtherStuff(startTime);
	}

	doStuff();

With the above code, the call stack would go through the following phases: 1. doStuff

  1. Date.now doStuff

  2. assign value to startTime doStuff

  3. doOtherStuff doStuff

  4. console.log doOtherStuff doStuff

  5. doOtherStuff doStuff

  6. doStuff

  7. [ empty ]

While the call stack is populated, no asynchronous code can be run.

So, no code that isn't directly called by an immediately preceding procedure will be run while the callstack is populated. Instead it is queued to be run once the call stack is empty

Callback Queue

The event loop

To manage

When you want to run code, the runtime sends that code to main thread call stack

that thread is busy running code it can't run other code. Any code that is waiting to be run sits in the callback queue. There are a number of ways to add code to this queue.

When the current code has completed running

setTimeout

requestAnimationFrame

setTimeout

@chartnett and @andrefuller if you want to use requestAnimationFrame I think you should replace both setTimeout()s with requestAnimationFrame()s (or rAFs for short). But I don't think changing them will change the result because of the effect on the call stack.

All rAF callbacks always run in the same or next frame of work Any rAFs queued in your event handlers will be executed in the ​same frame​. Any rAFs queued in a rAF will be executed in the next frame​. All rAFs that are queued will be executed in the intended frame.

https://medium.com/@paul_irish/requestanimationframe-scheduling-for-nerds-9c57f7438ef4#.y772nie5q

setTimeout will take the function you pass to it and add it to a queue to be run after the current call stack has completely emptied. The time value that you pass to it begins counting down the moment you call setTimeout but, as you probably know, doesn't mean it will run at that time. So

function doStuff () {
  var startTime = Date.now();
  setTimeout(function () {
    console.log('setTimeout called in ' + (Date.now() - startTime));
  }, 0);
  doOtherStuff(); // a function that takes about 3000ms to run
}

doStuff();
  • startTime would be populated
  • setTimeout will add the function to a queue
  • doOtherStuff() runs and takes 3000ms, completing the current call stack
  • the procedural code completes and the call stack is empty
  • the JS engine looks to the queue and runs the function added by setTimeout
  • "setTimeout called in 3000ms" is logged to the console

So knowing that and looking at https://github.com/chartnett/compass/blob/37c2a16ed18b61269445fe5a1fb3a64e70ccf2e7/src/js/modules/creatives/componentBased/views/list.js#L144-L189 we can see on line 149 we add multiple functions to the queue with

setTimeout(function () {
  getColumn = componentGrid.getColumnByField(column);
  if (getColumn) {
    componentGrid.removeColumn(getColumn);
  }
}, 0);

Say it creates 4 functions then the queue looks like: [funcA_1, funcA_2, funcA_3, funcA_4].

Then the call stack moves on to line 167 and adds another function to the queue.

Now the queue looks like [funcA_1, funcA_2, funcA_3, funcA_4, funcB_1]

The call stack will complete and the JS engine will look at its queue, remove funcA_1 and add it to the call stack, then run it.

As you probably know, when Strand components get their values changed here this triggers the Polymer library to do some magic.

Now (here's the fun part), Polymer code relies on rAF (@dlasky mailed out some info about it on July 27th, 2016).

So Polymer functions will be added to ANOTHER queue, the rAF queue.

An "animation frame" (effectively a screen redraw) aims to be done about every 16ms (1000ms/60fps). It only runs when the call stack is emptied. The rAF queue is a collection of the functions we want to run just before the screen is next redrawn. The functions are run in the order they're added.

Once it has been been 16ms since the last animation frame, the rAF queue (if it exists) is inserted at the front of the queue.

12ms since last animation frame

  • call stack is busy
  • rAF queue is [funcC_1]
  • queue is [funcA_1, funcA_2, funcA_3, funcA_4, funcB_1]

13ms since last animation frame

  • call stack is busy and adds to rAF queue
  • rAF queue is now [funcC_1, funcC_2]
  • queue is [funcA_1, funcA_2, funcA_3, funcA_4, funcB_1]

16ms since last animation frame

  • call stack is busy and adds to rAF queue
  • rAF queue is now [funcC_1, funcC_2, funcC_3]
  • queue is now [referenceTo_rAF_queue, funcA_1, funcA_2, funcA_3, funcA_4, funcB_1]

20ms since last animation frame

// call stack is freed and runs the first item from queue, the rAF queue of [funcC_1, funcC_2, funcC_3] // rAF queue is now [] // queue is now [funcA_1, funcA_2, funcA_3, funcA_4, funcB_1]

If funcAs take 16+ms to run then

funcA_1() // adds Polymer code to rAF queue
// call stack empties
// >> rAF queued functions are run

funcA_2() // adds Polymer code to rAF queue
// call stack empties
// >> rAF queued functions are run

If funcAs take 8ms to run then

funcA_1() // adds Polymer code to rAF queue
// call stack empties
funcA_2() // adds Polymer code to rAF queue
// call stack empties
// >> rAF queued functions are run

funcA_3() // adds Polymer code to rAF queue
funcA_4() // adds Polymer code to rAF queue
// call stack empties
// >> rAF queued functions are run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment