Angular,
The Event Loop,
&
You

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?


					
^

setTimeout(myCallback, 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);
}
					

The Rendering Pipeline


const worker = new Worker('job.js');
          

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 tasks
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);
          

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);
					

  browser.classList.remove('slide');

  requestAnimationFrame(() => {
    browser.classList.add('slide');
  });			
					

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!
  • Use Web Workers
  • Promises are microtasks
  • requestAnimationFrame for animations

Thanks!

ejzimmer.github.io/angular-event-loop-talk
What does this have to do with
?
<ul>
  <li>Tyrannosaurus Rex</li>
  <li>Velociraptor</li>
  <li>Triceratops</li>
<li>Brontosaurus</li>
</ul>
const dinosaurs = [
  'Tyrannosaurus Rex', 
  'Velociraptor', 
  'Triceratops',
  
'Brontosaurus'
];
?

setState(updatedState);
          

  const app = new Vue({
    name: 'Dr Ellie Sattler'
  });

  Object.defineProperty(
    app, 
    'items',
    { get: () => this._name,
      set: (value) => {
        this._name = value;
        runChangeDetection(); 
      }
    }
  );
ZONE.JS

A Zone is an execution context that persists across async tasks.

You can think of it as thread-local storage for JavaScript VMs.

A Zone is a mechanism for intercepting and keeping track of work.

In its simplest form a Zone allows one to intercept the scheduling and calling of asynchronous operations, and execute additional code before as well as after the asynchronous code.

window.setTimeout(callback, delay)

const nativeTimeout = window.setTimeout;
window.setTimeout = 
  function (callback, delay) {
    nativeTimeout(() => {
      preTaskCallback();
      callback();
      postTaskCallback();
    }, delay);
  }

const nativeTimeout = window.setTimeout;
window.setTimeout = 
  function (callback, delay) {
    nativeTimeout(() => {
      callback();
      runChangeDetection();
    }, delay);
  }
  
  • setTimeout & setInterval
  • addEventListener & onClick, etc
  • XMLHttpRequest & FileReader
  • Promises & requestAnimationFrame
  • registerElement/IDB/WebSocket/MutationObserver

platformBrowserDynamic().bootstrapModule(AppModule);
          

ngZone.run()
          
So what? ¯\_(ツ)_/¯
How does this affect me as an Angular developer? Short answer - it doesn't. You can write perfectly good Angular code without knowing any of this stuff. There is, however, one little trap that you should probably be aware of
Testing
Tests don't run in the ngZone, so you don't get any automatic change detection
On the one hand, this is a good thing, because it means your tests run faster.

it('should add a new list item', () => {
  component.dinosaurs.push('Brontosaurus');
  expect(listItems[3]).toBe('Brontosaurus');
});
          
Expected undefined to be 'Brontosaurus'
On the other hand, this can be a bit of a nuisance, because you can't test any functionality that changes the DOM
fixture.detectChanges()
Luckily, the Angular team though of this and provided you with a way to kick off the change detection manually

it('should add a new list item', (done) => {
  component.dinosaurs.push('Brontosaurus');
  fixture.detectChanges().then(() => {
    expect(listItemElements[3]).toBe('Brontosaurus');
    done();
  });
});
          
fakeAsync

it('should flush microtasks before returning', () => {
  let thenRan = false;
  fakeAsync(() => { 
    resolvedPromise.then(_ => { thenRan = true; }); 
  })();
  expect(thenRan).toEqual(true);
});
          
it('should add a new list item', fakeAsync((done) => {
  component.dinosaurs.push('Brontosaurus');
  fixture.detectChanges().then(() => {
    expect(listItemElements[3]).toBe('Brontosaurus');
    done();
  });
}));
          
The fakeAsync zone also provides you with functions that give you fine-grained control over how tasks are executed within the zone

it('should do something after a promise resolves', fakeAsync(() => {
  component.submit();
  flushMicrotasks();
  expect(dinosaurs).toContain('Torontosaurus');
}));            
          
Can't wait til the end of your task for all your promise callbacks to be run? Use flushMicrotasks to run them any time you like!j

          let count = 0;

          function countUp() {
            setTimeout(() => count++, 1000);
            setTimeout(() => count++, 2000);
            setTimeout(() => count++, 3000);
            setTimeout(() => count++, 4000);
            setTimeout(() => count++, 5000);
          }

          it('should count to 5 in five seconds', fakeAsync(() => {
            countUp();
            const timeElapsed = flush();

            expect(count).toBe(5);
            expect(timeElapsed).toBe(5000);
          }));
          
  • setTimeout(callback, 1000)
  • setTimeout(callback, 2000)
  • setTimeout(callback, 3000)
  • setTimeout(callback, 4000)
  • setTimeout(callback, 5000)
0
Or maybe it's not just microtasks that you're waiting on. Maybe you've got some timers or event callbacks that you're waiting on. Turns out there's a function for that too!
  • setTimeout(callback, 1000)
  • setTimeout(callback, 2000)
  • setTimeout(callback, 3000)
  • setTimeout(callback, 4000)
  • setTimeout(callback, 5000)
Performance

this.zone.runOutsideAngular(() => {
  paint();
});
          

this.zone.run(() => {
  calculate();
});         
          
Fun
a deep understanding of the event loop and zones probably isn't vital, but sometimes poking around under the covers can really help you see the bigger picture
Thanks!