Asynchronous programming is one of the main points of the language precisely because Javascript runs in a single thread. If there is only one thread to execute your code, you need to avoid as much as possible that code blocks the thread. Hence, time-consuming operations such as HTTP requests and disk access, or a database, are typically performed asynchronously.
It is good to clarify that Javascript code is always processed by a single thread, but this does not mean that the engine language and its host application (host) Just use a thread. For example, if a Node.js application requires disk access, Node may well use another thread to perform this access. But the code that requests this access and the callback code that handles the result run on that single thread dedicated to Javascript code.
This single thread executes a loop of events (Event loop).
There is a separate component responsible for popular event queue, popularly called Event Pump ("event pump", because "pumps" events to the queue). According to Wikipedia, it is typically implemented as a separate thread. Therefore, Event Pump and Event loop operate asynchronously in relation to each other. The Event loop is responsible for executing the Javascript code, processing one event at a time, according to the queue.
The loop of events would be something like this:
while(true) {
// Existem eventos na fila?
// Se sim, pega o código do primeiro da fila e executa de maneira síncrona.
}
Every iteration of the loop (tick), the engine check if there is an event in the queue. In the browser, this can be a timer expired, an XHR request response, or an interface event (such as an Event Listener one-click on some element). In Javascript on the server there are still other cases, such as asynchronous access to the disk or to a database. If there are events in the queue, the first of the queue is processed, and the callback is executed synchronously. Each tick loop has its own function execution stack. At the end of each tick the stack is always empty.
It is easier to understand with an example. Consider the following code, which creates a timer whose callback changes the value of the variable x
and shall be executed within 500 milliseconds:
var x = 0, i;
setTimeout(function() {
x = 10;
}, 500);
for(i=0; i<100; i++) {
// faz algo
}
This is processed as follows:
Tick 0: Empty event queue. Creates variables x
and i
; assign value 0
to x
; schedules the timer callback to run at 500ms; executes the body of the loop for
100 times.
Tick 1: Empty event queue (not passed 500ms). Does nothing.
(...)
Tick N: 1 event (expired timer) in the queue. Executes the pending callback, ie assigns value 10
to the variable x
captured via closure.
Note that there is no guarantee that the callback of our timer will run exactly 500ms after the start of the timer. It will run at the first possible opportunity when the condition is satisfied. For example, if the body of the loop for
perform some slow operation that takes 600ms to perform, our callback will run on tick 1, about 600ms after scheduling (ie delayed compared to scheduling).
With promises the engine is exactly the same, since promises in Javascript are just a more complex way, but with cleaner syntax, to use callbacks. Nor were they a native construction of the language in Ecmascript 5, only included in the following versions.
References
Related: What is the difference between asynchronous and synchronous communication?
– Marconi