Como o JavaScript funciona: uma visão geral do motor, do tempo de execução e da pilha de chamadas
A medida que o JavaScript se torna cada vez mais popular, equipes estão aproveitando seu suporte em vários níveis em sua pilha de tecnologias - front-end, back-end, aplicativos híbridos, dispositivos embarcados e muito mais.
Este post é o primeiro de uma série que tem como objetivo explorar mais profundamente o JavaScript e como ele realmente funciona: acreditamos que, ao conhecer os blocos de construção do JavaScript e como eles se combinam, você poderá escrever códigos e aplicativos melhores.
Como mostrado nas estatísticas do GitHut, o JavaScript está no topo em termos de repositórios ativos e envios totais no GitHub. Ele também está nas primeiras posições em outras categorias.
(Confira as estatísticas de linguagem atualizadas do GitHub).
Se os projetos estão se tornando cada vez mais dependentes do JavaScript, isso significa que os desenvolvedores precisam utilizar tudo o que a linguagem e o ecossistema oferecem, com um entendimento cada vez mais profundo de suas partes internas, a fim de construir softwares melhores.
Acontece que há muitos desenvolvedores que usam o JavaScript diariamente, mas não possuem conhecimento sobre o que acontece por debaixo dos panos.
Visão geral
Quase todo mundo já ouviu falar do Motor V8 como um conceito, e a maioria das pessoas sabe que o JavaScript é single-thread ou que ele usa uma fila de callbacks.
Neste post, vamos passar por todos esses conceitos em detalhes e explicar como o JavaScript é executado na prática. Ao conhecer esses detalhes, você será capaz de escrever aplicativos melhores e não bloqueantes, que aproveitam adequadamente as API's fornecidas.
Se você é relativamente novo no JavaScript, este post ajudará você a entender por que o JavaScript é tão "estranho" em comparação com outras linguagens.
E se você é um desenvolvedor experiente em JavaScript, esperamos que isso lhe traga novas perspectivas sobre como o Runtime JavaScript que você usa todos os dias realmente funciona.
Motor JavaScript
Um exemplo popular de um Motor JavaScript é o motor V8 da Google. O motor V8 é usado dentro do Chrome e do Node.js, por exemplo. Aqui está uma visão muito simplificada de como ele se parece:
O Motor consiste em dois componentes principais:
- Heap de Memória - é onde ocorre a alocação de memória.
- Pilha de Chamadas - é onde estão os quadros de pilha (stack frames) à medida que seu código é executado.
Tempo de Execução (Runtime)
Existem API's no navegador que são usadas por quase todos os desenvolvedores JavaScript (por exemplo, "setTimeout"). No entanto, essas APIs não são fornecidas pelo Motor.
Então, de onde elas vêm?
Acontece que a realidade é um pouco mais complicada.
Então, temos o Motor, mas na verdade há muito mais. Temos essas coisas chamadas API's da Web que são fornecidas pelos navegadores, como o DOM, AJAX, setTimeout e muito mais.
E então, temos o famoso event loop e a fila de callbacks.
Pilha de Chamadas (Call Stack)
JavaScript é uma linguagem de programação de thread único (single-thread), o que significa que possui uma única Pilha de Chamadas. Portanto, executa apenas uma tarefa de cada vez.
A Pilha de Chamadas é uma estrutura de dados, que basicamente registra em que parte do programa estamos. Se entramos em uma função, a colocamos no topo da pilha. Se retornamos de uma função, removemos o elemento do topo da pilha. Isso é tudo o que a pilha pode fazer.
Vamos ver um exemplo. Dê uma olhada no código a seguir:
function multiply(x, y) {
return x * y;
}
function printSquare(x) {
var s = multiply(x, x);
console.log(s);
}
printSquare(5);
Quando o motor começa a executar este código, a Pilha de Chamadas estará vazia. Em seguida, os passos serão os seguintes:
Cada entrada na pilha de chamadas é denominada de Quadro de Pilha (stack frame).
E é exatamente assim que os rastreamentos de pilha (Stack Traces) são construídos quando uma exceção é lançada - basicamente, é o estado da Pilha de Chamadas quando a exceção ocorreu. Dê uma olhada no código a seguir:
function foo() {
throw new Error('SessionStack will help you resolve crashes :)');
}
function bar() {
foo();
}
function start() {
bar();
}
start();
Se isso for executado no Chrome (assumindo que este código está em um arquivo chamado foo.js), o seguinte rastreamento de pilha será gerado:
"Estourando a pilha" (Blowing the stack) - isso ocorre quando você atinge o tamanho máximo da Pilha de Chamadas. E isso pode acontecer facilmente, especialmente se você estiver usando recursão sem testar seu código de forma abrangente. Dê uma olhada neste código de exemplo:
function foo() {
foo();
}
foo();
Quando o motor começa a executar este código, ele começa chamando a função "foo". No entanto, essa função é recursiva e começa a se chamar sem nenhuma condição de término. Assim, a cada passo da execução, a mesma função é adicionada repetidamente à Pilha de Chamadas. Isso se parece com algo assim:
Em algum momento, no entanto, o número de chamadas de função na Pilha de Chamadas excede o tamanho real da Pilha de Chamadas, e o navegador decide tomar uma ação, lançando um erro, que pode se parecer com isso:
Executar código em uma única thread pode ser relativamente fácil, pois você não precisa lidar com cenários complicados que surgem em ambientes de várias threads, como, por exemplo, deadlocks.
No entanto, executar em uma única thread também tem suas limitações. Como o JavaScript possui apenas uma Pilha de Chamadas, o que acontece quando as coisas ficam lentas?
Concorrência e Event Loop
O que acontece quando você tem chamadas de função na Pilha de Chamadas que levam uma quantidade enorme de tempo para serem processadas? Por exemplo, imagine que você queira fazer uma transformação complexa de imagem com JavaScript no navegador.
Você pode se perguntar: por que isso é um problema? O problema é que, enquanto a Pilha de Chamadas tem funções para executar, o navegador não pode fazer mais nada - ele fica bloqueado. Isso significa que o navegador não pode renderizar, nem pode executar nenhum outro código, ele simplesmente fica parado. E isso é um problema se você deseja ter uma interface de usuário fluida e agradável em seu aplicativo.
E esse não é o único problema. Uma vez que o navegador começa a processar tantas tarefas na Pilha de Chamadas, ele pode deixar de responder por um longo período de tempo. E a maioria dos navegadores toma uma ação, como gerar um erro, perguntando se você deseja encerrar a página web.
Essa não é a melhor experiência para o usuário, certo?
Então, como podemos executar um código pesado sem bloquear a interface do usuário e tornar o navegador irresponsivo? Bem, a solução são os callbacks assíncronos.
Isso será explicado com mais detalhes na Parte 2 do tutorial "Como o JavaScript realmente funciona": "Dentro do motor V8 + 5 dicas sobre como escrever código otimizado".
Referências
Este é um artigo traduzido. O artigo original pode ser lido no link abaixo.
Autor do post original — Alexander Zlatkov— Co-founder & CEO @SessionStack