JavaScript Event Loop

Sep 26, 2020 · 4 min read · 760 Words · -Views -Comments

I recently saw a basic question and realized my understanding of the JS event loop was not solid. So I organized it here to reinforce the fundamentals.

The basic question

Here is the original question:

console.log('start');
setTimeout(() => {
  console.log('children2');
  Promise.resolve().then(() => {
    console.log('children3');
  });
}, 0);

new Promise(function (resolve, reject) {
  console.log('children4');
  setTimeout(function () {
    console.log('children5');
    resolve('children6');
  }, 0);
}).then((res) => {
  console.log('children7');
  setTimeout(() => {
    console.log(res);
  }, 0);
});

Correct answer

start
children4
children2
children3
children5
children7
children6

Explanation

  • Macro task prints start
  • setTimeout enters the EventTable and registers, timer starts
  • Promise executor runs immediately, prints children4, then setTimeout enters EventTable. First round ends
  • First timer enters queue, prints children2, promise.then added to micro queue, prints children3
  • Second timer runs, prints children5, promise.then added to micro queue, prints children7
  • Timer fires, added to macro task, prints children6

Key concepts

  1. JS is single-threaded. The event loop is JS’s execution mechanism, and other languages differ (for example, Java supports multithreading).

  2. Synchronous and asynchronous tasks go into different execution contexts.

  3. Async tasks can be divided into macro tasks and micro tasks.

    Example: A bank clerk handling customers is a macro task. If a customer adds a small extra request, that can be considered a micro task.

    Each macro task has a micro task queue. After each macro task finishes, the micro tasks are drained before the next macro task.

  4. Macro tasks: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering

    Micro tasks: process.nextTick (Node), Promises, queueMicrotask, MutationObserver

  5. Even with 0 delay, setTimeout does not run immediately. Only after the call stack is empty will tasks be pulled from the Event Queue.

  6. Promise represents the final completion (or failure) of an async operation and its result.

  7. The event loop consists of the main thread and task queue. The main thread keeps pulling tasks from the queue and executing them.

Practice questions

Q1

NOTE: Run in Node.js.

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

Correct answer

1
7
6
8
2
4
3
5
9
11
10
12

Explanation

  • Macro task prints 1

  • setTimeouts go into the delay queue

  • process.nextTick creates a micro task (not executed yet)

  • Promise executor runs immediately, prints 7, .then creates a micro task

  • Another setTimeout goes into the delay queue

  • No more sync code, run micro tasks: 6, 8

  • Task stack cleared, timers are ready, move to message queue and execute

  • Print 2, 4

  • Run micro tasks: 3, 5

  • Print 9, 11

  • Run micro tasks: 10, 12

Q2

console.log('script start');

setTimeout(function () {
  console.log('setTimeout');
}, 0);

Promise.resolve()
  .then(function () {
    console.log('promise1');
  })
  .then(function () {
    console.log('promise2');
  });

console.log('script end');

Correct answer

script start
script end
promise1
promise2
setTimeout

Explanation

  • Macro task prints script start
  • Timer goes into event table
  • promise.then is a micro task, not executed until macro task ends
  • Macro task prints script end
  • Macro task ends, run micro tasks: promise1, promise2
  • Micro tasks end, timer fires, go to event queue, execute and print setTimeout

Q3

function foo() {
 setTimeout(foo, 0);
   }
foo();

function foo() {
 return Promise.resolve().then(foo);
 }
foo();

Snippet 1 will not overflow the stack, but snippet 2 will.

Explanation

  • setTimeout is a macro task, Promise is a micro task. Macro tasks are pushed one per loop cycle, but micro tasks are always drained before returning to the event loop. If you keep adding micro tasks faster than you process them, the loop never returns to rendering and you will overflow.

Non-blocking?

Now that we understand the event loop and concurrency model, let’s mention non-blocking. When talking about Node.js, people often mention async non-blocking.

Note: async and non-blocking are two concepts. JS is single-threaded, so only one thing runs at a time. To improve responsiveness, async exists. Async means you can continue without waiting for results, so it is non-blocking.

What is blocking? For example, in the browser, if you call alert, nothing else can happen until the user clicks OK. That is blocking.

Note that async non-blocking is a JS feature. Browser JS also has it.

Final Thoughts

This post is rough. If there are issues, please point them out.

References

Authors
Developer, digital product enthusiast, tinkerer, sharer, open source lover