⚙️ JavaScript por de baixo dos panos (Está tudo certinho?)
Olá pessoal!
Ontem enquanto via um vídeo sobre stack e heap, me bateu curiosidade de conhecer melhor os detalhes de como a linguagem que estou focando agora funciona, o JS da rapazeada kk.
Depois de ler bastante e assistir alguns vídeos, fiz um resumão anotando tudo que eu entendi e gostaria de compartilhar com vocês, até mesmo para caso queiram complementar ou corrigir algo! Segue:
JavaScript por de baixo dos panos
JavaScript é uma linguagem cujo runtime funciona de forma Single Thread, com um Event Loop.
O que isso, quer dizer? Para entender melhor, é necessário conhecer as partes que compõe o processo que roda nosso código JS:
Call Stack: Armazena as chamadas de funções utilizando a estrutura de dados de pilha, funcionando por LIFO. Em que as funções vão se resolvendo por ordem em que foram colocadas, com a última função colocada sendo a primeira a ser retirada da Stack assim que for resolvida;
Web APIs (Ou C++ APIs em um ambiente Node.js): É o que permite rodar o JavaScript de forma assíncrona quando necessário. São APIs conhecidas, como por exemplo: Timing Functions (setTimeout, setInterval, etc), fetch API, DOM API, event listeners e entre outras. Elas rodam em threads separadas utilizadas pelo navegador (Ou Node), voltando para o main thread ao jogar os resultados para a Callback Queue (ou Task Queue) quando o processamento é finalizado;
Callback Queue (ou Task Queue): É uma fila de de callbacks gerados pelas Web APIs. A cada processamento feito por uma Web API, seu callback (Função a ser realizada ou processamento a ser concluído pelo Main thread) é colocado na fila, para ser adicionado à Call Stack posteriormente, quando a mesma estiver vazia;
Render Queue: É uma fila de frames do render, basicamente é chamada (se a call stack estiver vazia) a cada 16 ms (Resultando em 60 frames por segundo) ou menos, dependendo do monitor do usuário. Ela que renderiza os frames da interface, tendo prioridade de chamada pelo Event Loop em relação à Callback queue;
Event Loop: Por conta do runtime JS funcionar de forma single thread, basicamente cada chamada de função deve ser realizada “uma de cada vez” nesse main thread. Assim, para permitir processamento assíncrono, existe o event loop, que verifica constantemente a call stack e a callback queue. Quando a call stack estiver vazia, significa que o processamento síncrono já foi realizado, então ele pega os itens da callback queue e vai jogando na call stack para serem processados e resolvidos um de cada vez, na ordem em que foram colocados na fila. Isso funciona como um loop, resolvendo todas as chamadas da call stack e callback queue até as duas estiverem vazias.
Conclusão
JavaScript realmente funciona de forma single thread, com o main thread sendo o orquestrador das funções que utilizamos para construir a interface ou processarmos dados. Porém, é possível utilizar funções assíncronas por conta de Web APIs (Browser) ou C++ APIs (Node), que faz uma parte do processamento em threads separadas do próprio navegador ou do Node, e depois jogam seus callbacks na callback queue para então serem processadas e resolvidas quando possível.
Bônus
Alguns conceitos interessantes a mais:
Web Workers: É possível trabalhar de forma parecida com multi-thread com a Web API de Web Workers. Chamando ela, vc deixa o browser ou node criar uma nova thread com stack isolada, para processar de forma paralela. Comunicando com a main thread por mensagens.
Tipos de Compilação: Independente de uma linguagem ser compilada ou interpretada, no final ela sempre gera um bytecode para ser utilizado pela máquina. Há dois tipos de compilação: AOT (Ahead of Time) e JIT (Just in Time), em que ambas podem ser utilizadas em diferentes configurações ou diferentes compiladores em uma mesma linguagem de programação.
AOT é mais fácil de ser utilizada por linguagens que geram seu bytecode de forma direta, ou seja, linguagens sem interpretadores ou sem VM (Como o Java, com sua JVM), em que são compiladas no momento de build para depois o código binário ser executado.
Já o JIT é o principal meio de compilação para o JavaScript. Ele vai interpetando o script da linguagem e gerando o código binário em tempo real, porém possui otimizações feitas pelo runtime, como por exemplo verificar quais funções estão sendo chamadas com mais frequência para já deixá-las salvas em bytecode, não precisando compilá-las de forma repetida frequentemente.
Mesmo com as otimizações do JIT, AOT normalmente gera uma performance melhor por conta do código binário já estar pronto para ser executado, porém perdem alguns benefícios que o JIT pode oferecer, como alterações mais flexíveis do código em runtime e debug mais fácil.
Stack e Heap: Stack e Heap existem em praticamente todas as linguagens de programação e geralmente funcionam de forma parecida entre elas.
No caso de JavaScript, a Stack é a área da memória (RAM) em que são armazenados tipos primitivos e de tamanho fixo (number, boolean, string, etc), sendo armazenadas de forma sequencial. Além disso, ela também armazena ponteiros (Endereços de memória) para referenciar tipos mais complexos de dados, como Arrays, Objects, e Functions, que são tipos com tamanho dinâmico. Esses tipos são alocados no Heap, que é uma área da memória que não é sequencial e só consegue ser acessada por referência de ponteiros, consequentemente sendo menos rápida de ser acessada em comparação com a stack.
OBS: Stack faz parte do gerenciamento de memória do JavaScript, enquanto que a Call Stack, como visto anteriormente, é um mecanismo do interpretador do JavaScript para rastrear e orquestrar as chamadas de função.