Assíncrono JS sob o capô

Jan 27 2023
MDN define o fluxo de trabalho assíncrono como ..

MDN define o fluxo de trabalho assíncrono como

..iniciar uma tarefa potencialmente demorada e ainda ser capaz de responder a outros eventos enquanto essa tarefa é executada... Assim que a tarefa for concluída, o resultado será apresentado ao seu programa.

Mas como exatamente isso é alcançável na web com JavaScript?

Antes de mergulhar nas implementações assíncronas, é útil ter um entendimento básico do JS em um nível inferior.

Como funciona o JS?

O JavaScript é single-threaded e interpretado, o que significa que as instruções são executadas diretamente e não possuem etapa de compilação ex-ante. Tomemos como exemplo o V8, o motor mais popular.

Em um nível alto, o V8 analisa o código, cria uma árvore de sintaxe abstrata (AST) e a compila em bytecode otimizado com o interpretador. Há também um preparador, dois compiladores e armazenamento em cache que o tornam rápido (o V8 tem ótimos blogs sobre isso). Ele nos fornece apenas uma pilha de chamadas, heap e loop de eventos ( mais ou menos, na verdade não) . Podemos ignorar o heap para os propósitos desta toca de coelho: é um pedaço de memória não estruturada com referências a objetos e variáveis.

Ciclo de Eventos de Tempo de Execução

Ao contrário de Java ou C++, JS permite a execução de um evento por vez. Podemos ver isso em nossos navegadores traçando um caminho de chamada com console.trace():

// shoutout to ES6 arrow functions

foo = () => {
    console.log("Hello, World!");
    bar();
}
bar = () => {
    console.log("Goodbye, World!");
    trace();
}

trace = () => console.trace();

foo();

Hello, World!   VM1049:6
Goodbye, World! VM1049:11 

// console.trace
trace @ VM1049:11
bar @ VM1049:7
foo @ VM1049:3

      
                
call stack visualization

SetTimeout

Vamos executar os mesmos métodos com um atraso assíncrono de 5000 ms com setTimeout().

foo = () => {
    console.log("Hello, World!");
     setTimeout(bar, 5000);
}

bar = () => {
    console.log("Goodbye, World!");
    trace();
}

trace = () => console.trace();

foo();

Hello, World!   VM1526:2

// 3 days later... 
Goodbye, World!  VM1526:6

// console.trace 
trace @ VM1382:10
bar @ VM1382:6

// setTimeout is invoked
b @ main-base.bundle.UM3…oFn_e5gQ.12.js:1723
setTimeout (async)  
Cga.window.setTimeout @ main-base.bundle.UM3…toFn_e5gQ.12.js:516

// foo is pushed to the stack first
foo @ VM1382:3

setTimeoutnão faz parte do mecanismo JS, o que faz sentido, pois o mecanismo pode manipular um evento por vez. Os navegadores , no entanto, são multiencadeados e nos fornecem APIs da web para acessar as implementações setTimeout. Por exemplo, o Chrome usa o mecanismo V8 de thread único em uma arquitetura de vários processos, onde cada processo é altamente multithread. Nesse caso, isso foi chamado explicitamente com Cga.setTimeout @ main-base.bundle.blah-blah-blah...Virtually, isso nos permite usar vários threads.

O Ciclo de Eventos

Vamos reordenar nosso bloco de código:

foo = () => {
    console.log("Hello, World!");
     setTimeout(bar, 5000);
}

bar = () => {
    setTimeout(() => console.log("Goodbye, World!"), 5000);
    trace();
}

trace = () => console.trace();

foo();

// output of foo 
Hello, World! VM1649:11 

// console.trace 
trace @ VM1694:11
bar @ VM1694:8
b @ main-base.bundle.UM3…oFn_e5gQ.12.js:1723
setTimeout (async)  
Cga.window.setTimeout @ main-base.bundle.UM3…toFn_e5gQ.12.js:516
foo @ VM1694:3
// end trace 

// 3 days later...
VM1649:6 Goodbye, World!

O JRE inclui:

  • Motor JS (single-threaded)
  • Java Virtual Machine (JVM) — A máquina virtual que executa o código em tempo de execução. Aliás, é por isso que “VM” precede os logs de rastreamento.
  • API Web
  • fila de tarefas
  • Loop de eventos

A pilha de chamadas do nosso mecanismo está exibindo os eventos um por um. Depois de ver setTimeout(), o método, junto com o retorno de chamada (referência de função), é delegado à API da web.

Observe que o mecanismo não se importa mais com o modo como a implementação setTimeout da API lida com o evento e o retorno de chamada. O mecanismo segue para exibir o próximo evento e alimentá-lo para o Pacman (a JVM que executa o código).

Enquanto isso, a API usa seus threads para esperar 5000 ms e, posteriormente, os entrega à fila de tarefas. O loop de eventos vê que o evento entra na fila e o empurra de volta para a pilha de chamadas se e somente se a pilha de chamadas estiver vazia.

Você pode usar o divertido visualizador de Philip Roberts para ver tudo em ação. Além disso, confira sua palestra incrível no JSConf em 2014.