2024-02-27: My exploration of the event loop in JS, part 1

27 Feb 2024

Just a post about my exploration of the event loop.

How does JS work? It’s a single threaded runtime. It executes task 1 by 1 form the stack. This is important, whatever is in the stack that will run.

Example1, a classic 1

console.log("START");

setTimeout(()=>{
    console.log(`setTimeout`), 0
});

console.log("END");

Output:

START
END
setTimeout

The important thing in JS is that anything asynchronous is put aside for the event loop. 1st all synchronous code is run and then we get to the async stuff. setTimeout is used to run async code.

Example2, let’s focus only on the synchronous stuff:

function foo() {
    console.log("foo start");
    setTimeout(()=>{
        console.log(`setTimeout2`), 0
    });
    ['elem1', 'elem2'].forEach((elem, idx)=>{
        console.log(`Array element ${idx}: ${elem}`)
    });
    console.log("foo end");
}

console.log("START");

setTimeout(()=>{
    console.log(`setTimeout1`), 0
});

foo();

console.log("END");

can you guess the output?

START
foo start
Array element 0: elem1
Array element 1: elem2
foo end
END
setTimeout1
setTimeout2

As you can see the sync code runs 1st and then the async code.

Anything async go back in line to the task queues.

Note: Not all callbacks are asynchronous.

Event Loop

After the synchronous code has run we get the event loop. This loop puts tasks in the stack when the stack is empty and there is a task in the task queues.

Stack empty? yes, task available? yes, then a task goes to the stack and it is run.

How do we know which task to run 1st? It’s based on task queues.

ok, wait, why do we need this kind of a process? Why not run everything synchronously?

JS is a single threaded runtime, if everything is run synchronously then we are blocking everything else, that’s a bad feature when we want to create a server to handle multiple requests, or on a browser when we want to render elements in the browser regularly, detect user input, make HTTP requests etc. It’s better to break the code in small pieces, do multiple things in a loop.

In a browser in 1 event loop we would want to make HTTP requests, refresh the page, handle events, etc. then continue in the next loop.

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

Task Queues

So we talked about how the event loop gets the tasks from task queues.

There are a few different task queues, the have different types of tasks and a few different rules on how they are handled.

The main queues:

  1. Macro task queue: setTimeout, setInterval, setImmediate
  2. Promise micro task queue: Promises
  3. process.nextTick queue: also a micro task queue, has process.nextTick tasks

Important points:

  1. 1 event loop 1 task from macro queue
  2. in 1 event loop all the micro tasks are run until they are exhausted, if new micro tasks are generated while processing these they will also be run. Note: we can get stuck in an infinite loop and block all processes if we keep generating microtasks

setTimeout

run task after a given amount of time

setInterval

run a task after a set interval

setImmediate

immediately run the task in the next event loop

Promise

add task after resolving a process

nextTick

run task immediately after completing the current operation in ths same event loop

Note: I will keep improving this article, it’s very barebones and missing quite a bit, if you have any questions ask away!

Please checkout these 1st:

Questions

Example3: can you guess the output here? It’s a big code I know and the explanations haven’t been perfect, I would recommend trying this after going through the resources. But I will say there are a few things which don’t add up in my head?

console.log("START");

let count = 0;

function nextTickFunc() {
    count++;
    console.log(`nextTick0 loop:${count}`);
}

setImmediate(()=>{
    console.log(`setImmediate0 loop:${count}`);
});

setTimeout(()=>{
    console.log(`setTimeout0 loop:${count}`), 0
});

loopTracker = process.nextTick(()=>{
    nextTickFunc();
});

setInterval(function () {
    process.nextTick(()=>{
        nextTickFunc();
    });
    console.log(`setInterval0 loop:${count}`);
    if (count > 3) {
        clearInterval(this)
    }
}, 0);

new Promise((resolve, reject) => {
    resolve(`Promise0 loop:${count}`);
}).then(resolve => {
    console.log(resolve);
    process.nextTick(()=>{
        console.log(`nextTick1 loop:${count}`);
        new Promise((resolve, reject) => {
            resolve(`Promise3 loop:${count}`);
        }).then(resolve => {
            console.log(resolve);
        });
        process.nextTick(()=>{
            console.log(`nextTick2 loop:${count}`);
            process.nextTick(()=>{
                console.log(`nextTick3 loop:${count}`);
            });
        });
    });
    new Promise((resolve, reject) => {
        resolve(`Promise1 loop:${count}`);
    }).then(resolve => {
        console.log(resolve);
        new Promise((resolve, reject) => {
            resolve(`Promise2 loop:${count}`);
        }).then(resolve => {
            console.log(resolve);
        });
    });
});

console.log("END");

output:

START
END
nextTick0 loop:1
Promise0 loop:0
Promise1 loop:1
Promise2 loop:1
nextTick1 loop:1
nextTick2 loop:1
nextTick3 loop:1
Promise3 loop:1
setTimeout0 loop:1
setInterval0 loop:1
nextTick0 loop:2
setImmediate0 loop:2
setInterval0 loop:2
nextTick0 loop:3
setInterval0 loop:3
nextTick0 loop:4
setInterval0 loop:4
nextTick0 loop:5
  1. in the 4th line of the output, why is the count 0 Promise0 loop:0?
  2. Why do we have the following
    Promise0 loop:0
    Promise1 loop:1
    

    and not the following?

    Promise0 loop:0
    nextTick1 loop:1
    
  3. why is it setImmediate0 loop:2 and not setImmediate0 loop:1?
  4. why did we stop at nextTick0 loop:5 and not nextTick0 loop:4?

I have an idea about 2, 3, 4 but I don’t know about 1, if you have any opinions please share! I’d love to discuss.

PS: What can I use to make quick animations for illustration purposes? I would also like something to simulate on the frontend with? It would be nice to make it interactive. It should be something easy to use, quick to get the job done. Currently exploring P5.js