Fun Adventures with

The Event Loop

  • Erin Zimmer
  • twitter.com/ErinJZimmer
  • event-loop.ez.codes

What actually is the Event Loop?

The Browser
Web APIs
The Browser
JavaScript Engine
Web APIs
^
JavaScript Engine

while (true) {
 task = taskQueue.pop();
 execute(task);
}
					
  • What's a task?
  • What's a task queue?
  • How do tasks
    get in the task queue?


					
^

JavaScript, why are you like that?

JavaScript is single-threaded

- But remember, the JS engine is only one part of the browser

Browsers are multi-threaded

As well as running your JavaScript, a browser could be - keeping track of mouse and keyboard events - making network requests - handling timers - disk operations And any of these things could be generating tasks, via callbacks
The Browser
In fact, this idea is the very heart of the asynchronous programming we all know and love so much
setTimeout(myCallbackFunction, 3000);
Hey WebAPIs! Could you wait 3 seconds and then run my callback function?
No worries! You keep doing your thing, I'll take care of this.

while (true) {
 task = taskQueue.pop();
 execute(task);
}
					
So that's basically how tasks and task queues work. Of course, in real life, it's a bit more complicated than that

The Rendering Pipeline

Long running tasks will cause your browser to start dropping frames and, much like this cat, your app just won't run right
function repeat(reps, action) {
 for (let i = 0; i < reps; i++) {
   action();
 }
}					
function repeat(reps, action) {
 action();
 if (--reps) {
  setTimeout(() => repeat(reps, action));
 }
}					

/*  job.js  */
onmessage = function(e) {
 for (let i = 0; i < e.data.reps; i++) {
  e.data.action();
 }
}

/*  app.js  */
const worker = new Worker('job.js');
worker.postMessage(action, reps);
					

while (true) {
 task = taskQueue.pop();
 execute(task);

 if (isRepaintTime()) repaint();
}
					
An event loop has one or more task queues.

bool did_work = delegate->DoWork();
if (!keep_running_)
 break;
did_work |= delegate->DoDelayedWork(&delayed_work_time_);
if (!keep_running_)
 break;
if (did_work)
 continue;
did_work = delegate->DoIdleWork();
if (!keep_running_)
 break;
					

Multiple task queues

  • Queues can be executed in any order
  • Tasks in the same queue must be executed in the order they arrived
  • Tasks from the same source
    must go in the same queue

while (true) {
 queue = getNextQueue();
 task = queue.pop();
 execute(task);

 if (isRepaintTime()) repaint();
}
					

Microtasks

A task that happens between a task and rendering
Between one task and the next, or between task and rendering

const observer = new MutationObserver(callback);
const myElement = document.getElementById('stegosaurus');
observer.observe(myElement, ({ subtree: true }));
					
Potentially lots of things happening Changes to DOM, want to run things related to changing DOM before window renders again

const myPromise = new Promise((resolve, reject) => { ... });
myPromise.then(callback).catch(errorCallback);
					
Performance reasons Esp catch -> want error handling to happen after stuff, but before anything else

window.queueMicrotask(callback);
					
generally intended for people writing frameworks

Tasks
vs
Microtasks


while (true) {
 queue = getNextQueue();
 task = queue.pop();
 execute(task);

 while (microtaskQueue.hasTasks())
  doMicrotask();

 if (isRepaintTime()) repaint();
}
					

Animation Frame Callback Queue


requestAnimationFrame(callback);
					
Why would we want to do this? Well, imagine we wanted to make a nice animation of a box moving along a path.
while (box.style.right < screen.width) {
 const elapsedTime = Date.now() - startTime;
 box.style.left = calculateX(elapsedTime);
 box.style.top = calculateY(elapsedTime)
}

					
If you did that, what you would get is this...
function move() {
 const elapsedTime = Date.now() - startTime;
 box.style.left = calculateX(elapsedTime);
 box.style.top = calculateY(elapsedTime);
 if (box.style.right < screen.width)
  setTimeout(move);
}
					
function move() {
 const elapsedTime = Date.now() - startTime;
 box.style.left = calculateX(elapsedTime);
 box.style.top = calculateY(elapsedTime);
 if (x < screen.width)
  requestAnimationFrame(move);
 }
}

while (true) {
 queue = getNextQueue();
 task = queue.pop();
 execute(task);

 while (microtaskQueue.hasTasks()) 
  doMicrotask();

 if (isRepaintTime()) {
  animationTasks = animationQueue.copyTasks();
  for (task in animationTasks) 
   doAnimationTask(task);
		
  repaint();
 }
}
					
  • Don't block rendering
  • Promises beat tasks
  • Animate with requestAnimationFrame
console.error node_modules/react-dom/cjs/react-dom.development.js:530
Warning: An update to MyComponent inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://fb.me/react-wrap-tests-with-act
    in MyComponent 
          

Thanks! (rawr)