JavaScript sob o capô

Nov 28 2022
Índice Neste artigo, vamos nos aprofundar no funcionamento interno do JavaScript e como ele realmente é executado. Ao entender os detalhes, você entenderá o comportamento do seu código e, portanto, poderá escrever aplicativos melhores.

Índice

  • Thread e Pilha de Chamadas
  • Contexto de Execução
  • Loop de eventos e JavaScript assíncrono
  • Armazenamento de memória e coleta de lixo
  • Compilação JIT (Just In Time)
  • Resumo

Neste artigo, vamos nos aprofundar no funcionamento interno do JavaScript e como ele realmente é executado. Ao entender os detalhes, você entenderá o comportamento do seu código e, portanto, poderá escrever aplicativos melhores.

JavaScript é descrito como:

Linguagem de programação single-threaded, coleta de lixo, interpretada ou compilada Just In Time com um loop de eventos sem bloqueio.

Vamos descompactar cada um desses termos-chave.

Pilha de conversas e chamadas:

O mecanismo JavaScript é um interpretador de thread único composto por um heap e uma única pilha de chamadas que é usada para executar o programa.
A pilha de chamadas é uma estrutura de dados que usa o princípio Last In, First Out (LIFO) para armazenar e gerenciar temporariamente a invocação de função (chamada).
Isso significa que a última função que é colocada na pilha é a primeira a aparecer quando a função retornar.
Como a pilha de chamadas é única, a execução da(s) função(ões) é feita uma de cada vez, de cima para baixo. Isso significa que a pilha de chamadas é síncrona.

Agora, como é síncrono, você se perguntará como o JavaScript pode lidar com chamadas assíncronas?
Bem, o loop de eventos é o segredo por trás da programação assíncrona do JavaScript.
Mas antes de explicar o conceito de chamadas assíncronas dentro do JavaScript e como isso é possível com a linguagem single-threaded, vamos entender primeiro como o código é executado.

Contexto de Execução (EC):

O Contexto de Execução é definido como o ambiente no qual o código JavaScript é executado.
A criação de um Contexto de Execução acontece em duas fases:

1. Fase de Criação de Memória:

  • Criando o objeto global (que é chamado de objeto janela no navegador e objeto global no NodeJS).
  • Criando o objeto “this” e vinculando-o ao objeto global.
  • Configurando heap de memória (Um heap é uma região de memória grande, principalmente não estruturada) para armazenar variáveis ​​e referências de funções.
  • Armazenando funções e variáveis ​​no contexto de execução global implementando Hoisting .

Agora que conhecemos as etapas por trás da execução do código, vamos voltar ao

Circuito de eventos:

Primeiro, vamos começar olhando para este diagrama:

Loop de eventos em JS

Temos o mecanismo que consiste em dois componentes principais:
* Memory Heap — é aqui que ocorre a alocação de memória.
* Call Stack — é onde seus quadros de pilha estão enquanto seu código é executado.

Temos as APIs da Web, que são threads que você não pode acessar, basta fazer chamadas para elas. Eles são as partes do navegador nas quais a simultaneidade entra em ação, como DOM, AJAX, setTimeout e muito mais.

Por fim, há a fila de retorno de chamada, que é uma lista de eventos a serem processados. Cada evento tem uma função associada que é chamada para tratá-lo.

Então, qual é a tarefa do loop de eventos aqui?
O Event Loop tem uma tarefa simples — monitorar a Call Stack e a Callback Queue. Se a pilha de chamadas estiver vazia, o loop de eventos pegará o primeiro evento da fila e o enviará para a pilha de chamadas, que efetivamente o executará.
Tal iteração é chamada de tick no Event Loop. Cada evento é apenas um retorno de chamada de função.

Armazenamento de memória e coleta de lixo:

Para entender a necessidade da coleta de lixo, devemos primeiro entender o ciclo de vida da memória, que é praticamente o mesmo para qualquer linguagem de programação, possui 3 etapas principais.
1. Aloque a memória.
2. Use a memória alocada para ler ou gravar ou ambos.
3. Libere a memória alocada quando ela não for mais necessária.

A maioria dos problemas de gerenciamento de memória ocorre quando tentamos liberar a memória alocada. A principal preocupação que surge é a determinação dos recursos de memória não utilizados.
No caso das linguagens de baixo nível em que o desenvolvedor precisa decidir manualmente quando a memória não é mais necessária, as linguagens de alto nível, como JavaScript, usam uma forma automatizada de gerenciamento de memória conhecida como Garbage Collection (GC).
O JavaScript usa duas estratégias famosas para executar GC: a técnica de contagem de referência e o algoritmo Mark-and-sweep.
Aqui está uma explicação detalhada do MDN sobre ambos os algoritmos e como eles funcionam.

Compilação JIT (Just In Time):

Vamos voltar à definição de JavaScript: ela diz “Linguagem de programação interpretada e compilada por JIT”, então o que isso significa? Que tal começar com a diferença entre um compilador e um interpretador em geral?

Como analogia, pense em duas pessoas com idiomas diferentes que desejam se comunicar. Compilar é como parar e dedicar todo o tempo para aprender o idioma, e interpretar será como ter alguém ali para interpretar cada frase.

Portanto, as linguagens compiladas têm um tempo de gravação lento e um tempo de execução rápido e as linguagens interpretadas têm o oposto.

Falando em termos técnicos: a compilação é um processo de conversão do código-fonte do programa em código binário legível por máquina, antes da execução, e um compilador pega todo o programa de uma só vez.

Por outro lado, um interpretador é um programa que executa as instruções do programa sem exigir que elas sejam pré-compiladas em um formato legível por máquina e usa uma única linha de código por vez.

E aqui vem a função de compilação JIT que está melhorando o desempenho dos programas interpretados. Todo o código é convertido em código de máquina de uma só vez e executado imediatamente .

Dentro do compilador JIT, temos um novo componente chamado monitor (também conhecido como criador de perfil). Esse monitor observa o código enquanto ele é executado e

  • Identifique os componentes quentes ou mornos do código, por exemplo: código repetitivo.
  • Transforme esses componentes em código de máquina durante o tempo de execução.
  • Otimize o código de máquina gerado.
  • Hot swap a implementação anterior do código.

Agora que entendemos os conceitos principais, vamos dedicar um minuto para juntar tudo e resumir as etapas que o JS Engine segue ao executar o código:

Fonte da imagem: canal de mídia traversy
  1. O JS Engine pega o código JS escrito em sintaxe legível por humanos e o transforma em código de máquina.
  2. O mecanismo usa um analisador para percorrer o código linha por linha e verificar se a sintaxe está correta. Se houver algum erro, o código interromperá a execução e um erro será lançado.
  3. Se todas as verificações forem aprovadas, o analisador cria uma estrutura de dados em árvore chamada Abstract Syntax Tree (AST).
  4. O AST é uma estrutura de dados que representa o código em uma estrutura semelhante a uma árvore. É mais fácil transformar código em código de máquina a partir de um AST.
  5. O interpretador então pega o AST e o transforma em IR, que é uma abstração do código de máquina e um intermediário entre o código JS e o código de máquina. O IR também permite realizar otimizações e é mais móvel.
  6. O compilador JIT pega o IR gerado e o transforma em código de máquina, compilando o código, obtendo feedback em tempo real e usando esse feedback para melhorar o processo de compilação.

Obrigado por ler :)

Você pode me seguir no Twitter e no LinkedIn .