Understanding Event Queue and Call stack in javascript
Answer 1 & 3
There is a very big difference between event queue and call stack. In fact, they have almost nothing in common.
Call stack (simple overview):
When you execute a function everything it uses is said to go on the stack, which is the same call stack you're referring to there. Very simplified, it's temporary memory for functional execution. Or in other words
function foo() {
console.log("-> start [foo]");
console.log("<- end [foo]");
}
foo();
when called it would be given a little sandbox to play with on the stack. When the function ends, the temporary memory used would be wiped and made available to other things. So, the resources used (unless given somewhere to the system) will only last as long as the function lasts.
Now, if you have nested functions
function foo() {
console.log("-> start [foo]");
console.log("<- end [foo]");
}
function bar() {
console.log("-> start [bar]");
foo()
console.log("<- end [bar]");
}
bar();
Here is what happens when you call the function:
bar
gets executed - memory is allocated on the stack for it.bar
prints "start"foo
gets executed - memory is allocated on the stack for it. NB!bar
is still running and its memory is also there.foo
prints "start"foo
prints "end"foo
finishes execution and its memory is cleared from the stack.bar
prints "end"bar
finishes execution and its memory is cleared from the stack.
So, the execution order is bar
-> foo
but the resolution is in last in, first out order (LIFO) foo
finishes -> bar
finishes.
And this is what makes it a "stack".
The important thing to note here is that the resources used by a function will only be released when it finishes executing. And it finishes execution when all the functions inside it and those inside them finish executing. So you could have a very deep call stack like a
-> b
-> c
-> d
-> e
and if any large resources are held in a
you would need b
to e
to finish before they are released.
In recursion a function calls itself which still makes entries on the stack. So, if a
keeps calling itself you end up with a call stack of a
-> a
-> a
-> a
etc.
Here is a very brief illustration
// a very naive recursive count down function
function recursiveCountDown(count) {
//show that we started
console.log("-> start recursiveCountDown [" + count + "]");
if (count !== 0) {//exit condition
//take one off the count and recursively call again
recursiveCountDown(count -1);
console.log("<- end recursiveCountDown [" + count + "]"); // show where we stopped. This will terminate this stack but only after the line above finished executing;
} else {
console.log("<<<- it's the final recursiveCountDown! [" + count + "]"); // show where we stopped
}
}
console.log("--shallow call stack--")
recursiveCountDown(2);
console.log("--deep call stack--")
recursiveCountDown(10);
This is a very simplistic and very flawed recursive function but it only serves to demonstrate what happens in that case.
Event queue
JavaScript is ran in an event queue (or also "event loop") which, in simple terms, waits for "activity" (events), processes them and then waits again.
If there are multiple events, it would process them in order - first in, first out (FIFO), hence a queue. So, if we re-write the above functions:
function foo() {
console.log("-> start [foo]");
console.log("<- end [foo]");
}
function bar() {
console.log("-> start [bar]");
console.log("<- end [bar]");
}
function baz() {
console.log("-> start [baz]");
setTimeout(foo, 0);
setTimeout(bar, 0);
console.log("<- end [baz]");
}
baz();
Here is how this plays out.
baz
is executed. Memory allocated on the stack.foo
is delayed by scheduling it to run "next".bar
is delayed by scheduling it to run "next".baz
finishes. Stack is cleared.- Event loop picks the next item on the queue - this is
foo
. foo
gets executed. Memory allocated on the stack.foo
finishes. Stack is cleared.- Event loop picks the next item on the queue - this is
bar
. bar
gets executed. Memory allocated on the stack.bar
finishes. Stack is cleared.
As you can hopefully see, the stack is still in play. Any function you call will always generate a stack entry. The event queue is a separate mechanism.
With this way of doing things, you get less memory overhead, since you do not have to wait on any other function to release the allocated resources. On the other hand, you cannot rely on any function being complete.
I hope this section also answers your Q3.
Answer 2
How does deferring to the queue help?
I hope the above explanation will make it more clear but it bears making sure the explanation makes sense:
There is a set limit of how deep the stack could be. If you think about it, it should be obvious - there is only so much memory to spare for presumably temporary storage. Once the maximum call depth is reached, JavaScript will throw RangeError: Maximum call stack size exceeded
error.
If you look at the recursiveCountDown
example I gave above that can very easily be made to cause an error - if you call recursiveCountDown(100000)
you will get the RangeError
.
By putting every other execution on the queue, you avoid filling up the stack and thus you avoid the RangeError
. So let's re-write the function
// still naive but a bit improved recursive count down function
function betterRecursiveCountDown(count) {
console.log("-> start recursiveCountDown [" + count + "]");
if (count !== 0) {
//setTimeout takes more than two parameters - anything after the second one will be passed to the function when it gets executed
setTimeout(betterRecursiveCountDown, 0, count - 1);
console.log("<- end recursiveCountDown [" + count + "]");
} else {
console.log("<<<- it's the final recursiveCountDown! [" + count + "]"); // show where we stopped
}
}
betterRecursiveCountDown(10);